feat: add VbenForm component (#4352)

* feat: add form component

* fix: build error

* feat: add form adapter

* feat: add some component

* feat: add some component

* feat: add example

* feat: suppoer custom action button

* chore: update

* feat: add example

* feat: add formModel,formDrawer demo

* fix: build error

* fix: typo

* fix: ci error

---------

Co-authored-by: jinmao <jinmao88@qq.com>
Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>
This commit is contained in:
Vben
2024-09-10 21:48:51 +08:00
committed by GitHub
parent 86ed732ca8
commit 524b9badf2
271 changed files with 5974 additions and 1247 deletions

View File

@@ -0,0 +1,21 @@
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'],
},
],
});

View File

@@ -0,0 +1,50 @@
{
"name": "@vben-core/form-ui",
"version": "5.2.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/@vben-core/uikit/form-ui"
},
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"prepublishOnly": "npm run build"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
}
},
"publishConfig": {
"exports": {
".": {
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@vben-core/composables": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vee-validate/zod": "^4.13.2",
"@vueuse/core": "^11.0.3",
"vee-validate": "^4.13.2",
"vue": "^3.5.3",
"zod": "^3.23.8",
"zod-defaults": "^0.1.3"
}
}

View File

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

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import { computed, toRaw, unref } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import { VbenExpandableArrow } from '@vben-core/shadcn-ui';
import { cn, isFunction } from '@vben-core/shared/utils';
import { COMPONENT_MAP } from '../config';
import { injectFormProps } from '../use-form-context';
const { $t } = useSimpleLocale();
const [rootProps, form] = injectFormProps();
const collapsed = defineModel({ default: false });
const resetButtonOptions = computed(() => {
return {
show: true,
text: `${$t.value('reset')}`,
...unref(rootProps).resetButtonOptions,
};
});
const submitButtonOptions = computed(() => {
return {
show: true,
text: `${$t.value('submit')}`,
...unref(rootProps).submitButtonOptions,
};
});
const isQueryForm = computed(() => {
return !!unref(rootProps).showCollapseButton;
});
const queryFormStyle = computed(() => {
if (isQueryForm.value) {
return {
'grid-column': `-2 / -1`,
marginLeft: 'auto',
};
}
return {};
});
async function handleSubmit(e: Event) {
e?.preventDefault();
e?.stopPropagation();
const { valid } = await form.validate();
if (!valid) {
return;
}
await unref(rootProps).handleSubmit?.(toRaw(form.values));
}
async function handleReset(e: Event) {
e?.preventDefault();
e?.stopPropagation();
const props = unref(rootProps);
if (isFunction(props.handleReset)) {
await props.handleReset?.(form.values);
} else {
form.resetForm();
}
}
</script>
<template>
<div
:class="cn('col-span-full w-full text-right', rootProps.actionWrapperClass)"
:style="queryFormStyle"
>
<component
:is="COMPONENT_MAP.DefaultResetActionButton"
v-if="resetButtonOptions.show"
class="mr-3"
type="button"
@click="handleReset"
v-bind="resetButtonOptions"
>
{{ resetButtonOptions.text }}
</component>
<component
:is="COMPONENT_MAP.DefaultSubmitActionButton"
v-if="submitButtonOptions.show"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
>
{{ submitButtonOptions.text }}
</component>
<VbenExpandableArrow
v-if="rootProps.showCollapseButton"
v-model:model-value="collapsed"
class="ml-2"
>
<span>{{ collapsed ? $t('expand') : $t('collapse') }}</span>
</VbenExpandableArrow>
</div>
</template>

View File

@@ -0,0 +1,65 @@
import type { BaseFormComponentType, VbenFormAdapterOptions } from './types';
import type { Component } from 'vue';
import { h } from 'vue';
import {
VbenButton,
VbenCheckbox,
Input as VbenInput,
VbenInputPassword,
VbenPinInput,
VbenSelect,
} from '@vben-core/shadcn-ui';
import { defineRule } from 'vee-validate';
const DEFAULT_MODEL_PROP_NAME = 'modelValue';
export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
DefaultResetActionButton: h(VbenButton, { size: 'sm', variant: 'outline' }),
DefaultSubmitActionButton: h(VbenButton, { size: 'sm', variant: 'default' }),
VbenCheckbox,
VbenInput,
VbenInputPassword,
VbenPinInput,
VbenSelect,
};
export const COMPONENT_BIND_EVENT_MAP: Partial<
Record<BaseFormComponentType, string>
> = {
VbenCheckbox: 'checked',
};
export function setupVbenForm<
T extends BaseFormComponentType = BaseFormComponentType,
>(options: VbenFormAdapterOptions<T>) {
const { components, config, defineRules } = options;
if (defineRules) {
for (const key of Object.keys(defineRules)) {
defineRule(key, defineRules[key as never]);
}
}
const baseModelPropName =
config?.baseModelPropName ?? DEFAULT_MODEL_PROP_NAME;
const modelPropNameMap = config?.modelPropNameMap as
| Record<BaseFormComponentType, string>
| undefined;
for (const component of Object.keys(components)) {
const key = component as BaseFormComponentType;
COMPONENT_MAP[key] = components[component as never];
if (baseModelPropName !== DEFAULT_MODEL_PROP_NAME) {
COMPONENT_BIND_EVENT_MAP[key] = baseModelPropName;
}
// 覆盖特殊组件的modelPropName
if (modelPropNameMap && modelPropNameMap[key]) {
COMPONENT_BIND_EVENT_MAP[key] = modelPropNameMap[key];
}
}
}

View File

@@ -0,0 +1,175 @@
import type {
FormState,
GenericObject,
ResetFormOpts,
ValidationOptions,
} from 'vee-validate';
import type { FormActions, VbenFormProps } from './types';
import { toRaw } from 'vue';
import { Store } from '@vben-core/shared/store';
import { bindMethods, isFunction, StateHandler } from '@vben-core/shared/utils';
function getDefaultState(): VbenFormProps {
return {
actionWrapperClass: '',
collapsed: false,
collapsedRows: 1,
commonConfig: {},
handleReset: undefined,
handleSubmit: undefined,
layout: 'horizontal',
resetButtonOptions: {},
schema: [],
showCollapseButton: false,
showDefaultActions: true,
submitButtonOptions: {},
wrapperClass: 'grid-cols-1',
};
}
export class FormApi {
// private prevState!: ModalState;
private state: null | VbenFormProps = null;
// private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;
public form = {} as FormActions;
isMounted = false;
stateHandler: StateHandler;
public store: Store<VbenFormProps>;
constructor(options: VbenFormProps = {}) {
const { ...storeState } = options;
const defaultState = getDefaultState();
this.store = new Store<VbenFormProps>(
{
...defaultState,
...storeState,
},
{
onUpdate: () => {
this.state = this.store.state;
},
},
);
this.state = this.store.state;
this.stateHandler = new StateHandler();
bindMethods(this);
}
private async getForm() {
if (!this.isMounted) {
// 等待form挂载
await this.stateHandler.waitForCondition();
}
if (!this.form?.meta) {
throw new Error('<VbenForm /> is not mounted');
}
return this.form;
}
// 如果需要多次更新状态,可以使用 batch 方法
batchStore(cb: () => void) {
this.store.batch(cb);
}
async getValues() {
const form = await this.getForm();
return form.values;
}
mount(formActions: FormActions) {
if (!this.isMounted) {
Object.assign(this.form, formActions);
this.stateHandler.setConditionTrue();
this.isMounted = true;
}
}
/**
* 根据字段名移除表单项
* @param fields
*/
async removeSchemaByFields(fields: string[]) {
const fieldSet = new Set(fields);
const schema = this.state?.schema ?? [];
const filterSchema = schema.filter((item) => fieldSet.has(item.fieldName));
this.setState({
schema: filterSchema,
});
}
/**
* 重置表单
*/
async resetForm(
state?: Partial<FormState<GenericObject>> | undefined,
opts?: Partial<ResetFormOpts>,
) {
const form = await this.getForm();
return form.resetForm(state, opts);
}
async resetValidate() {
const form = await this.getForm();
const fields = Object.keys(form.errors.value);
fields.forEach((field) => {
form.setFieldError(field, undefined);
});
}
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
const form = await this.getForm();
form.setFieldValue(field, value, shouldValidate);
}
setState(
stateOrFn:
| ((prev: VbenFormProps) => Partial<VbenFormProps>)
| Partial<VbenFormProps>,
) {
if (isFunction(stateOrFn)) {
this.store.setState(stateOrFn as (prev: VbenFormProps) => VbenFormProps);
} else {
this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
}
}
async setValues(
fields: Record<string, any>,
shouldValidate: boolean = false,
) {
const form = await this.getForm();
form.setValues(fields, shouldValidate);
}
async submitForm(e?: Event) {
e?.preventDefault();
e?.stopPropagation();
const form = await this.getForm();
await form.submitForm();
const rawValues = toRaw(form.values || {});
await this.state?.handleSubmit?.(rawValues);
return rawValues;
}
unmounted() {
this.state = null;
this.isMounted = false;
this.stateHandler.reset();
}
async validate(opts?: Partial<ValidationOptions>) {
const form = await this.getForm();
return await form.validate(opts);
}
}

View File

@@ -0,0 +1,24 @@
import type { FormRenderProps } from '../types';
import { computed } from 'vue';
import { createContext } from '@vben-core/shadcn-ui';
export const [injectRenderFormProps, provideFormRenderProps] =
createContext<FormRenderProps>('FormRenderProps');
export const useFormContext = () => {
const formRenderProps = injectRenderFormProps();
const isVertical = computed(() => formRenderProps.layout === 'vertical');
const componentMap = computed(() => formRenderProps.componentMap);
const componentBindEventMap = computed(
() => formRenderProps.componentBindEventMap,
);
return {
componentBindEventMap,
componentMap,
isVertical,
};
};

View File

@@ -0,0 +1,116 @@
import type {
FormItemDependencies,
FormSchemaRuleType,
MaybeComponentProps,
} from '../types';
import { computed, ref, watch } from 'vue';
import { isFunction } from '@vben-core/shared/utils';
import { useFormValues } from 'vee-validate';
import { injectRenderFormProps } from './context';
export default function useDependencies(
getDependencies: () => FormItemDependencies | undefined,
) {
const values = useFormValues();
const formRenderProps = injectRenderFormProps();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const formApi = formRenderProps.form!;
if (!values) {
throw new Error('useDependencies should be used within <VbenForm>');
}
const isIf = ref(true);
const isDisabled = ref(false);
const isShow = ref(true);
const isRequired = ref(false);
const dynamicComponentProps = ref<MaybeComponentProps>({});
const dynamicRules = ref<FormSchemaRuleType>();
const triggerFieldValues = computed(() => {
// 该字段可能会被多个字段触发
const triggerFields = getDependencies()?.triggerFields ?? [];
return triggerFields.map((dep) => {
return values.value[dep];
});
});
const resetConditionState = () => {
isDisabled.value = false;
isIf.value = true;
isShow.value = true;
isRequired.value = false;
dynamicRules.value = undefined;
dynamicComponentProps.value = {};
};
watch(
[triggerFieldValues, getDependencies],
async ([_values, dependencies]) => {
if (!dependencies || !dependencies?.triggerFields?.length) {
return;
}
resetConditionState();
const {
componentProps,
disabled,
if: whenIf,
required,
rules,
show,
trigger,
} = dependencies;
// 1. 优先判断if如果if为false则不渲染dom后续判断也不再执行
const formValues = values.value;
if (isFunction(whenIf)) {
isIf.value = !!(await whenIf(formValues, formApi));
// 不渲染
if (!isIf.value) return;
}
// 2. 判断show如果show为false则隐藏
if (isFunction(show)) {
isShow.value = !!(await show(formValues, formApi));
if (!isShow.value) return;
}
if (isFunction(componentProps)) {
dynamicComponentProps.value = await componentProps(formValues, formApi);
}
if (isFunction(rules)) {
dynamicRules.value = await rules(formValues, formApi);
}
if (isFunction(disabled)) {
isDisabled.value = !!(await disabled(formValues, formApi));
}
if (isFunction(required)) {
isRequired.value = !!(await required(formValues, formApi));
}
if (isFunction(trigger)) {
await trigger(formValues, formApi);
}
},
{ deep: true, immediate: true },
);
return {
dynamicComponentProps,
dynamicRules,
isDisabled,
isIf,
isRequired,
isShow,
};
}

View File

@@ -0,0 +1,97 @@
import type { FormRenderProps } from '../types';
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
/**
* 动态计算行数
*/
export function useExpandable(props: FormRenderProps) {
const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef');
const rowMapping = ref<Record<number, number>>({});
// 是否已经计算过一次
const isCalculated = ref(false);
const breakpoints = useBreakpoints(breakpointsTailwind);
const keepFormItemIndex = computed(() => {
const rows = props.collapsedRows ?? 1;
const mapping = rowMapping.value;
let maxItem = 0;
for (let index = 1; index <= rows; index++) {
maxItem += mapping?.[index] ?? 0;
}
return maxItem - 1;
});
watch(
[
() => props.showCollapseButton,
() => breakpoints.active().value,
() => props.schema?.length,
],
async ([val]) => {
if (val) {
await nextTick();
rowMapping.value = {};
await calculateRowMapping();
}
},
);
async function calculateRowMapping() {
if (!props.showCollapseButton) {
return;
}
await nextTick();
if (!wrapperRef.value) {
return;
}
// 小屏幕不计算
if (breakpoints.smaller('sm').value) {
// 保持一行
rowMapping.value = { 1: 2 };
return;
}
const formItems = [...wrapperRef.value.children];
const container = wrapperRef.value;
const containerStyles = window.getComputedStyle(container);
const rowHeights = containerStyles
.getPropertyValue('grid-template-rows')
.split(' ');
const containerRect = container?.getBoundingClientRect();
formItems.forEach((el) => {
const itemRect = el.getBoundingClientRect();
// 计算元素在第几行
const itemTop = itemRect.top - containerRect.top;
let rowStart = 0;
let cumulativeHeight = 0;
for (const [i, rowHeight] of rowHeights.entries()) {
cumulativeHeight += Number.parseFloat(rowHeight);
if (itemTop < cumulativeHeight) {
rowStart = i + 1;
break;
}
}
if (rowStart > (props?.collapsedRows ?? 1)) {
return;
}
rowMapping.value[rowStart] = (rowMapping.value[rowStart] ?? 0) + 1;
isCalculated.value = true;
});
}
onMounted(() => {
calculateRowMapping();
});
return { isCalculated, keepFormItemIndex, wrapperRef };
}

View File

@@ -0,0 +1,283 @@
<script setup lang="ts">
import type { ZodType } from 'zod';
import type { FormSchema } from '../types';
import { computed } from 'vue';
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
VbenRenderContent,
} from '@vben-core/shadcn-ui';
import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
import { toTypedSchema } from '@vee-validate/zod';
import { useFormValues } from 'vee-validate';
import { injectRenderFormProps, useFormContext } from './context';
import useDependencies from './dependencies';
import FormLabel from './form-label.vue';
import { isEventObjectLike } from './helper';
interface Props extends FormSchema {}
const {
component,
componentProps,
dependencies,
description,
disabled,
fieldName,
formFieldProps,
label,
labelClass,
labelWidth,
renderComponentContent,
rules,
} = defineProps<Props>();
const { componentBindEventMap, componentMap, isVertical } = useFormContext();
const formRenderProps = injectRenderFormProps();
const values = useFormValues();
const formApi = formRenderProps.form;
const fieldComponent = computed(() => {
const finalComponent = isString(component)
? componentMap.value[component]
: component;
if (!finalComponent) {
// 组件未注册
console.warn(`Component ${component} is not registered`);
}
return finalComponent;
});
const {
dynamicComponentProps,
dynamicRules,
isDisabled,
isIf,
isRequired,
isShow,
} = useDependencies(() => dependencies);
const labelStyle = computed(() => {
return labelClass?.includes('w-') || isVertical.value
? {}
: {
width: `${labelWidth}px`,
};
});
const currentRules = computed(() => {
return dynamicRules.value || rules;
});
const shouldRequired = computed(() => {
if (!currentRules.value) {
return isRequired.value;
}
if (isRequired.value) {
return true;
}
if (isString(currentRules.value)) {
return currentRules.value === 'required';
}
let isOptional = currentRules?.value?.isOptional?.();
// 如果有设置默认值,则不是必填,需要特殊处理
const typeName = currentRules?.value?._def?.typeName;
if (typeName === 'ZodDefault') {
const innerType = currentRules?.value?._def.innerType;
if (innerType) {
isOptional = innerType.isOptional?.();
}
}
return !isOptional;
});
const fieldRules = computed(() => {
let rules = currentRules.value;
if (!rules) {
return isRequired.value ? 'required' : null;
}
if (isString(rules)) {
return rules;
}
const isOptional = !shouldRequired.value;
if (!isOptional) {
const unwrappedRules = (rules as any)?.unwrap?.();
if (unwrappedRules) {
rules = unwrappedRules;
}
}
return toTypedSchema(rules as ZodType);
});
const computedProps = computed(() => {
const finalComponentProps = isFunction(componentProps)
? componentProps(values.value, formApi!)
: componentProps;
return {
...finalComponentProps,
...dynamicComponentProps.value,
};
});
const shouldDisabled = computed(() => {
return isDisabled.value || disabled || computedProps.value?.disabled;
});
const customContentRender = computed(() => {
if (!isFunction(renderComponentContent)) {
return {};
}
return renderComponentContent(values.value, formApi!);
});
const renderContentKey = computed(() => {
return Object.keys(customContentRender.value);
});
const fieldProps = computed(() => {
const rules = fieldRules.value;
return {
keepValue: true,
label,
...(rules ? { rules } : {}),
...formFieldProps,
};
});
function fieldBindEvent(slotProps: Record<string, any>) {
const modelValue = slotProps.componentField.modelValue;
const handler = slotProps.componentField['onUpdate:modelValue'];
const bindEventField = isString(component)
? componentBindEventMap.value?.[component]
: null;
let value = modelValue;
// antd design 的一些组件会传递一个 event 对象
if (modelValue && isObject(modelValue) && bindEventField) {
value = isEventObjectLike(modelValue)
? modelValue?.target?.[bindEventField]
: modelValue;
}
if (bindEventField) {
return {
[`onUpdate:${bindEventField}`]: handler,
[bindEventField]: value,
onChange: (e: Record<string, any>) => {
const shouldUnwrap = isEventObjectLike(e);
const onChange = slotProps?.componentField?.onChange;
if (!shouldUnwrap) {
return onChange?.(e);
}
return onChange?.(e?.target?.[bindEventField] ?? e);
},
onInput: () => {},
};
}
return {};
}
function createComponentProps(slotProps: Record<string, any>) {
const bindEvents = fieldBindEvent(slotProps);
const binds = {
...slotProps.componentField,
...computedProps.value,
...bindEvents,
};
return binds;
}
</script>
<template>
<FormField
v-if="isIf"
v-bind="fieldProps"
v-slot="slotProps"
:name="fieldName"
>
<FormItem
v-show="isShow"
:class="{
'flex-col': isVertical,
'flex-row items-center': !isVertical,
}"
class="flex pb-6"
v-bind="$attrs"
>
<FormLabel
v-if="!hideLabel"
:class="
cn(
'flex leading-6',
{
'mr-2 flex-shrink-0': !isVertical,
'flex-row': isVertical,
},
!isVertical && labelClass,
)
"
:help="help"
:required="shouldRequired && !hideRequiredMark"
:style="labelStyle"
>
{{ label }}
</FormLabel>
<div :class="cn('relative flex w-full items-center', wrapperClass)">
<FormControl :class="cn(controlClass)">
<slot
v-bind="{
...slotProps,
...createComponentProps(slotProps),
disabled: shouldDisabled,
}"
>
<component
:is="fieldComponent"
v-bind="createComponentProps(slotProps)"
:disabled="shouldDisabled"
>
<template v-for="name in renderContentKey" :key="name" #[name]>
<VbenRenderContent
:content="customContentRender[name]"
v-bind="slotProps"
/>
</template>
<!-- <slot></slot> -->
</component>
</slot>
</FormControl>
<!-- 自定义后缀 -->
<div v-if="suffix" class="ml-1">
<VbenRenderContent :content="suffix" />
</div>
<FormDescription v-if="description">
<VbenRenderContent :content="description" />
</FormDescription>
<Transition name="slide-up">
<FormMessage class="absolute -bottom-[22px]" />
</Transition>
</div>
</FormItem>
</FormField>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { FormLabel, VbenHelpTooltip } from '@vben-core/shadcn-ui';
interface Props {
help?: string;
required?: boolean;
}
defineProps<Props>();
</script>
<template>
<FormLabel class="flex flex-row-reverse items-center">
<VbenHelpTooltip v-if="help" trigger-class="size-3.5 ml-1">
{{ help }}
</VbenHelpTooltip>
<slot></slot>
<span v-if="required" class="text-destructive mr-[2px]">*</span>
</FormLabel>
</template>

View File

@@ -0,0 +1,140 @@
<script setup lang="ts">
import type { GenericObject } from 'vee-validate';
import type { ZodTypeAny } from 'zod';
import type { FormRenderProps, FormSchema, FormShape } from '../types';
import { computed } from 'vue';
import { Form } from '@vben-core/shadcn-ui';
import { cn, isString, merge } from '@vben-core/shared/utils';
import { provideFormRenderProps } from './context';
import { useExpandable } from './expandable';
import FormField from './form-field.vue';
import { getBaseRules, getDefaultValueInZodStack } from './helper';
interface Props extends FormRenderProps {}
const props = withDefaults(defineProps<Props>(), {
collapsedRows: 1,
commonConfig: () => ({}),
showCollapseButton: false,
wrapperClass: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
});
const emits = defineEmits<{
submit: [event: any];
}>();
provideFormRenderProps(props);
const { isCalculated, keepFormItemIndex, wrapperRef } = useExpandable(props);
const shapes = computed(() => {
const resultShapes: FormShape[] = [];
props.schema?.forEach((schema) => {
const { fieldName } = schema;
const rules = schema.rules as ZodTypeAny;
let typeName = '';
if (rules && !isString(rules)) {
typeName = rules._def.typeName;
}
const baseRules = getBaseRules(rules) as ZodTypeAny;
resultShapes.push({
default: getDefaultValueInZodStack(rules),
fieldName,
required: !['ZodNullable', 'ZodOptional'].includes(typeName),
rules: baseRules,
});
});
return resultShapes;
});
const formComponent = computed(() => (props.form ? 'form' : Form));
const formComponentProps = computed(() => {
return props.form
? {
onSubmit: props.form.handleSubmit((val) => emits('submit', val)),
}
: {
onSubmit: (val: GenericObject) => emits('submit', val),
};
});
const formCollapsed = computed(() => {
return props.collapsed && isCalculated.value;
});
const computedSchema = computed((): FormSchema[] => {
const {
componentProps = {},
controlClass = '',
disabled,
formFieldProps = {},
formItemClass = '',
hideLabel = false,
hideRequiredMark = false,
labelClass = '',
labelWidth = 100,
wrapperClass = '',
} = props.commonConfig;
return (props.schema || []).map((schema, index): FormSchema => {
const keepIndex = keepFormItemIndex.value;
const hidden =
// 折叠状态 & 显示折叠按钮 & 当前索引大于保留索引
props.showCollapseButton && !!formCollapsed.value && keepIndex
? keepIndex <= index
: false;
return {
disabled,
hideLabel,
hideRequiredMark,
labelWidth,
wrapperClass,
...schema,
componentProps: merge({}, schema.componentProps, componentProps),
controlClass: cn(controlClass, schema.controlClass),
formFieldProps: {
...formFieldProps,
...schema.formFieldProps,
},
formItemClass: cn(
'flex-shrink-0',
{ hidden },
formItemClass,
schema.formItemClass,
),
labelClass: cn(labelClass, schema.labelClass),
};
});
});
</script>
<template>
<component :is="formComponent" v-bind="formComponentProps">
<div ref="wrapperRef" :class="wrapperClass" class="grid">
<template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot>
</div> -->
<FormField
v-bind="cSchema"
:class="cSchema.formItemClass"
:rules="cSchema.rules"
>
<template #default="slotProps">
<slot v-bind="slotProps" :name="cSchema.fieldName"> </slot>
</template>
</FormField>
</template>
<slot :shapes="shapes"></slot>
</div>
</component>
</template>

View File

@@ -0,0 +1,60 @@
import type {
AnyZodObject,
ZodDefault,
ZodEffects,
ZodNumber,
ZodString,
ZodTypeAny,
} from 'zod';
import { isObject, isString } from '@vben-core/shared/utils';
/**
* Get the lowest level Zod type.
* This will unpack optionals, refinements, etc.
*/
export function getBaseRules<
ChildType extends AnyZodObject | ZodTypeAny = ZodTypeAny,
>(schema: ChildType | ZodEffects<ChildType>): ChildType | null {
if (!schema || isString(schema)) return null;
if ('innerType' in schema._def)
return getBaseRules(schema._def.innerType as ChildType);
if ('schema' in schema._def)
return getBaseRules(schema._def.schema as ChildType);
return schema as ChildType;
}
/**
* Search for a "ZodDefault" in the Zod stack and return its value.
*/
export function getDefaultValueInZodStack(schema: ZodTypeAny): any {
if (!schema || isString(schema)) {
return;
}
const typedSchema = schema as unknown as ZodDefault<ZodNumber | ZodString>;
if (typedSchema._def.typeName === 'ZodDefault')
return typedSchema._def.defaultValue();
if ('innerType' in typedSchema._def) {
return getDefaultValueInZodStack(
typedSchema._def.innerType as unknown as ZodTypeAny,
);
}
if ('schema' in typedSchema._def) {
return getDefaultValueInZodStack(
(typedSchema._def as any).schema as ZodTypeAny,
);
}
return undefined;
}
export function isEventObjectLike(obj: any) {
if (!obj || !isObject(obj)) {
return false;
}
return Reflect.has(obj, 'target') && Reflect.has(obj, 'stopPropagation');
}

View File

@@ -0,0 +1,3 @@
export { default as Form } from './form.vue';
export { default as FormField } from './form-field.vue';
export { default as FormLabel } from './form-label.vue';

View File

@@ -0,0 +1,11 @@
export { setupVbenForm } from './config';
export type {
BaseFormComponentType,
FormSchema as VbenFormSchema,
VbenFormProps,
} from './types';
export * from './use-vben-form';
// export { default as VbenForm } from './vben-form.vue';
export * as z from 'zod';

View File

@@ -0,0 +1,327 @@
import type { VbenButtonProps } from '@vben-core/shadcn-ui';
import type { Field, FormContext, GenericObject } from 'vee-validate';
import type { ZodTypeAny } from 'zod';
import type { FormApi } from './form-api';
import type { Component, HtmlHTMLAttributes, Ref } from 'vue';
export type FormLayout = 'horizontal' | 'vertical';
export type BaseFormComponentType =
| 'DefaultResetActionButton'
| 'DefaultSubmitActionButton'
| 'VbenCheckbox'
| 'VbenInput'
| 'VbenInputPassword'
| 'VbenPinInput'
| 'VbenSelect'
| (Record<never, never> & string);
type Breakpoints = '' | '2xl:' | '3xl:' | 'lg:' | 'md:' | 'sm:' | 'xl:';
type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
export type WrapperClassType =
| `${Breakpoints}grid-cols-${GridCols}`
| (Record<never, never> & string);
export type FormItemClassType =
| `${Breakpoints}cols-end-${'auto' | GridCols}`
| `${Breakpoints}cols-span-${'auto' | 'full' | GridCols}`
| `${Breakpoints}cols-start-${'auto' | GridCols}`
| (Record<never, never> & string)
| WrapperClassType;
export interface FormShape {
/** 默认值 */
default?: any;
/** 字段名 */
fieldName: string;
/** 是否必填 */
required?: boolean;
rules?: ZodTypeAny;
}
export type MaybeComponentPropKey =
| 'options'
| 'placeholder'
| 'title'
| keyof HtmlHTMLAttributes
| (Record<never, never> & string);
export type MaybeComponentProps = { [K in MaybeComponentPropKey]?: any };
export type FormActions = FormContext<GenericObject>;
export type CustomRenderType = (() => Component | string) | string;
export type FormSchemaRuleType =
| 'required'
| null
| (Record<never, never> & string)
| ZodTypeAny;
type FormItemDependenciesCondition<T = boolean | PromiseLike<boolean>> = (
value: Partial<Record<string, any>>,
actions: FormActions,
) => T;
type FormItemDependenciesConditionWithRules = (
value: Partial<Record<string, any>>,
actions: FormActions,
) => FormSchemaRuleType | PromiseLike<FormSchemaRuleType>;
type FormItemDependenciesConditionWithProps = (
value: Partial<Record<string, any>>,
actions: FormActions,
) => MaybeComponentProps | PromiseLike<MaybeComponentProps>;
export interface FormItemDependencies {
/**
* 组件参数
* @returns 组件参数
*/
componentProps?: FormItemDependenciesConditionWithProps;
/**
* 是否禁用
* @returns 是否禁用
*/
disabled?: FormItemDependenciesCondition;
/**
* 是否渲染删除dom
* @returns 是否渲染
*/
if?: FormItemDependenciesCondition;
/**
* 是否必填
* @returns 是否必填
*/
required?: FormItemDependenciesCondition;
/**
* 字段规则
*/
rules?: FormItemDependenciesConditionWithRules;
/**
* 是否隐藏(Css)
* @returns 是否隐藏
*/
show?: FormItemDependenciesCondition;
/**
* 任意触发都会执行
*/
trigger?: FormItemDependenciesCondition<void>;
/**
* 触发字段
*/
triggerFields: string[];
}
type ComponentProps =
| ((
value: Partial<Record<string, any>>,
actions: FormActions,
) => MaybeComponentProps)
| MaybeComponentProps;
export interface FormCommonConfig {
/**
* 所有表单项的props
*/
componentProps?: ComponentProps;
/**
* 所有表单项的控件样式
*/
controlClass?: string;
/**
* 所有表单项的禁用状态
* @default false
*/
disabled?: boolean;
/**
* 所有表单项的控件样式
* @default ""
*/
formFieldProps?: Partial<typeof Field>;
/**
* 所有表单项的栅格布局
* @default ""
*/
formItemClass?: string;
/**
* 隐藏所有表单项label
* @default false
*/
hideLabel?: boolean;
/**
* 是否隐藏必填标记
* @default false
*/
hideRequiredMark?: boolean;
/**
* 所有表单项的label样式
* @default "w-[100px]"
*/
labelClass?: string;
/**
* 所有表单项的label宽度
*/
labelWidth?: number;
/**
* 所有表单项的wrapper样式
*/
wrapperClass?: string;
}
type RenderComponentContentType = (
value: Partial<Record<string, any>>,
api: FormActions,
) => Record<string, any>;
export type HandleSubmitFn = (
values: Record<string, any>,
) => Promise<void> | void;
export type HandleResetFn = (
values: Record<string, any>,
) => Promise<void> | void;
export interface FormSchema<
T extends BaseFormComponentType = BaseFormComponentType,
> extends FormCommonConfig {
/** 组件 */
component: Component | T;
/** 组件参数 */
componentProps?: ComponentProps;
/** 默认值 */
defaultValue?: any;
/** 依赖 */
dependencies?: FormItemDependencies;
/** 描述 */
description?: string;
/** 字段名 */
fieldName: string;
/** 帮助信息 */
help?: string;
/** 表单项 */
label?: string;
// 自定义组件内部渲染
renderComponentContent?: RenderComponentContentType;
/** 字段规则 */
rules?: FormSchemaRuleType;
/** 后缀 */
suffix?: CustomRenderType;
}
export interface FormFieldProps extends FormSchema {
required?: boolean;
}
export interface FormRenderProps<
T extends BaseFormComponentType = BaseFormComponentType,
> {
/**
* 是否展开在showCollapseButton=true下生效
*/
collapsed?: boolean;
/**
* 折叠时保持行数
* @default 1
*/
collapsedRows?: number;
/**
* 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
*/
commonConfig?: FormCommonConfig;
/**
* 组件v-model事件绑定
*/
componentBindEventMap?: Partial<Record<BaseFormComponentType, string>>;
/**
* 组件集合
*/
componentMap: Record<BaseFormComponentType, Component>;
/**
* 表单实例
*/
form?: FormContext<GenericObject>;
/**
* 表单项布局
*/
layout?: FormLayout;
/**
* 表单定义
*/
schema?: FormSchema<T>[];
/**
* 是否显示展开/折叠
*/
showCollapseButton?: boolean;
/**
* 表单栅格布局
* @default "grid-cols-1"
*/
wrapperClass?: WrapperClassType;
}
export interface ActionButtonOptions extends VbenButtonProps {
show?: boolean;
text?: string;
}
export interface VbenFormProps<
T extends BaseFormComponentType = BaseFormComponentType,
> extends Omit<
FormRenderProps<T>,
'componentBindEventMap' | 'componentMap' | 'form'
> {
/**
* 表单操作区域class
*/
actionWrapperClass?: any;
/**
* 表单重置回调
*/
handleReset?: HandleResetFn;
/**
* 表单提交回调
*/
handleSubmit?: HandleSubmitFn;
/**
* 重置按钮参数
*/
resetButtonOptions?: ActionButtonOptions;
/**
* 是否显示默认操作按钮
*/
showDefaultActions?: boolean;
/**
* 提交按钮参数
*/
submitButtonOptions?: ActionButtonOptions;
}
export type ExtendedFormApi = {
useStore: <T = NoInfer<VbenFormProps>>(
selector?: (state: NoInfer<VbenFormProps>) => T,
) => Readonly<Ref<T>>;
} & FormApi;
export interface VbenFormAdapterOptions<
T extends BaseFormComponentType = BaseFormComponentType,
> {
components: Partial<Record<T, Component>>;
config?: {
baseModelPropName?: string;
modelPropNameMap?: Partial<Record<T, string>>;
};
defineRules?: {
required?: (
value: any,
params: any,
ctx: Record<string, any>,
) => boolean | string;
};
}

View File

@@ -0,0 +1,59 @@
import type { FormActions, VbenFormProps } from './types';
import { computed, type ComputedRef, unref, useSlots } from 'vue';
import { createContext } from '@vben-core/shadcn-ui';
import { isString } from '@vben-core/shared/utils';
import { useForm } from 'vee-validate';
import { object, type ZodRawShape } from 'zod';
import { getDefaultsForSchema } from 'zod-defaults';
export const [injectFormProps, provideFormProps] =
createContext<[ComputedRef<VbenFormProps> | VbenFormProps, FormActions]>(
'VbenFormProps',
);
export function useFormInitial(
props: ComputedRef<VbenFormProps> | VbenFormProps,
) {
const slots = useSlots();
const initialValues = generateInitialValues();
const form = useForm({
...(Object.keys(initialValues)?.length ? { initialValues } : {}),
});
const delegatedSlots = computed(() => {
const resultSlots: string[] = [];
for (const key of Object.keys(slots)) {
if (key !== 'default') {
resultSlots.push(key);
}
}
return resultSlots;
});
function generateInitialValues() {
const initialValues: Record<string, any> = {};
const zodObject: ZodRawShape = {};
(unref(props).schema || []).forEach((item) => {
if (Reflect.has(item, 'defaultValue')) {
initialValues[item.fieldName] = item.defaultValue;
} else if (item.rules && !isString(item.rules)) {
zodObject[item.fieldName] = item.rules;
}
});
const schemaInitialValues = getDefaultsForSchema(object(zodObject));
return { ...initialValues, ...schemaInitialValues };
}
return {
delegatedSlots,
form,
};
}

View File

@@ -0,0 +1,49 @@
import type {
BaseFormComponentType,
ExtendedFormApi,
VbenFormProps,
} from './types';
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';
import { useStore } from '@vben-core/shared/store';
import { FormApi } from './form-api';
import VbenUseForm from './vben-use-form.vue';
export function useVbenForm<
T extends BaseFormComponentType = BaseFormComponentType,
>(options: VbenFormProps<T>) {
const IS_REACTIVE = isReactive(options);
const api = new FormApi(options);
const extendedApi: ExtendedFormApi = api as never;
extendedApi.useStore = (selector) => {
return useStore(api.store, selector);
};
const Form = defineComponent(
(props: VbenFormProps, { attrs, slots }) => {
onBeforeUnmount(() => {
api.unmounted();
});
return () =>
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
},
{
inheritAttrs: false,
name: 'VbenUseForm',
},
);
// Add reactivity support
if (IS_REACTIVE) {
watch(
() => options.schema,
() => {
api.setState({ schema: options.schema });
},
{ immediate: true },
);
}
return [Form, extendedApi] as const;
}

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import type { VbenFormProps } from './types';
import { ref, watchEffect } from 'vue';
import { useForwardPropsEmits } from '@vben-core/composables';
import FormActions from './components/form-actions.vue';
import { COMPONENT_BIND_EVENT_MAP, COMPONENT_MAP } from './config';
import { Form } from './form-render';
import { provideFormProps, useFormInitial } from './use-form-context';
// 通过 extends 会导致热更新卡死
interface Props extends VbenFormProps {}
const props = withDefaults(defineProps<Props>(), {
actionWrapperClass: '',
collapsed: false,
collapsedRows: 1,
commonConfig: () => ({}),
handleReset: undefined,
handleSubmit: undefined,
layout: 'horizontal',
resetButtonOptions: () => ({}),
showCollapseButton: false,
showDefaultActions: true,
submitButtonOptions: () => ({}),
wrapperClass: 'grid-cols-1',
});
const forward = useForwardPropsEmits(props);
const currentCollapsed = ref(false);
const { delegatedSlots, form } = useFormInitial(props);
provideFormProps([props, form]);
const handleUpdateCollapsed = (value: boolean) => {
currentCollapsed.value = !!value;
};
watchEffect(() => {
currentCollapsed.value = props.collapsed;
});
</script>
<template>
<Form
v-bind="forward"
:collapsed="currentCollapsed"
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
:component-map="COMPONENT_MAP"
:form="form"
>
<template
v-for="slotName in delegatedSlots"
:key="slotName"
#[slotName]="slotProps"
>
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
<template #default="slotProps">
<slot v-bind="slotProps">
<FormActions
v-if="showDefaultActions"
:model-value="currentCollapsed"
@update:model-value="handleUpdateCollapsed"
/>
</slot>
</template>
</Form>
</template>

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import type { ExtendedFormApi, VbenFormProps } from './types';
import { useForwardPriorityValues } from '@vben-core/composables';
import FormActions from './components/form-actions.vue';
import { COMPONENT_BIND_EVENT_MAP, COMPONENT_MAP } from './config';
import { Form } from './form-render';
import { provideFormProps, useFormInitial } from './use-form-context';
// 通过 extends 会导致热更新卡死,所以重复写了一遍
interface Props extends VbenFormProps {
formApi: ExtendedFormApi;
}
const props = defineProps<Props>();
const state = props.formApi?.useStore?.();
const forward = useForwardPriorityValues(props, state);
const { delegatedSlots, form } = useFormInitial(forward);
provideFormProps([forward, form]);
props.formApi?.mount?.(form);
const handleUpdateCollapsed = (value: boolean) => {
props.formApi?.setState({ collapsed: !!value });
};
</script>
<template>
<Form
v-bind="forward"
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
:component-map="COMPONENT_MAP"
:form="form"
>
<template
v-for="slotName in delegatedSlots"
:key="slotName"
#[slotName]="slotProps"
>
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
<template #default="slotProps">
<slot v-bind="slotProps">
<FormActions
v-if="forward.showDefaultActions"
:model-value="state.collapsed"
@update:model-value="handleUpdateCollapsed"
/>
</slot>
</template>
</Form>
</template>

View File

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

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -22,7 +22,7 @@ import {
import { useNamespace } from '@vben-core/composables';
import { Ellipsis } from '@vben-core/icons';
import { isHttpUrl } from '@vben-core/shared';
import { isHttpUrl } from '@vben-core/shared/utils';
import { useResizeObserver } from '@vueuse/core';
@@ -430,7 +430,7 @@ $namespace: vben;
--menu-item-padding-x: 12px;
--menu-item-popup-padding-y: 20px;
--menu-item-popup-padding-x: 12px;
--menu-item-margin-y: 3px;
--menu-item-margin-y: 2px;
--menu-item-margin-x: 0px;
--menu-item-collapse-padding-y: 23.5px;
--menu-item-collapse-padding-x: 0px;
@@ -475,7 +475,7 @@ $namespace: vben;
&.is-rounded {
--menu-item-margin-x: 8px;
--menu-item-collapse-margin-x: 6px;
--menu-item-radius: 10px;
--menu-item-radius: 8px;
}
&.is-horizontal:not(.is-rounded) {

View File

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

View File

@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DrawerApi } from '../drawer-api';
// 模拟 Store 类
vi.mock('@vben-core/shared', () => {
vi.mock('@vben-core/shared/store', () => {
return {
isFunction: (fn: any) => typeof fn === 'function',
Store: class {

View File

@@ -1,6 +1,7 @@
import type { DrawerApiOptions, DrawerState } from './drawer';
import { isFunction, Store } from '@vben-core/shared';
import { Store } from '@vben-core/shared/store';
import { bindMethods, isFunction } from '@vben-core/shared/utils';
export class DrawerApi {
private api: Pick<
@@ -58,13 +59,14 @@ export class DrawerApi {
},
},
);
this.state = this.store.state;
this.api = {
onBeforeClose,
onCancel,
onConfirm,
onOpenChange,
};
bindMethods(this);
}
// 如果需要多次更新状态,可以使用 batch 方法

View File

@@ -5,10 +5,10 @@ import { ref, watch } from 'vue';
import {
useIsMobile,
usePriorityValue,
usePriorityValues,
useSimpleLocale,
} from '@vben-core/composables';
import { Info, X } from '@vben-core/icons';
import { X } from '@vben-core/icons';
import {
Sheet,
SheetClose,
@@ -18,12 +18,12 @@ import {
SheetHeader,
SheetTitle,
VbenButton,
VbenHelpTooltip,
VbenIconButton,
VbenLoading,
VbenTooltip,
VisuallyHidden,
} from '@vben-core/shadcn-ui';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
interface Props extends DrawerProps {
class?: string;
@@ -42,20 +42,22 @@ const { $t } = useSimpleLocale();
const { isMobile } = useIsMobile();
const state = props.drawerApi?.useStore?.();
const title = usePriorityValue('title', props, state);
const description = usePriorityValue('description', props, state);
const titleTooltip = usePriorityValue('titleTooltip', props, state);
const showFooter = usePriorityValue('footer', props, state);
const showLoading = usePriorityValue('loading', props, state);
const closable = usePriorityValue('closable', props, state);
const modal = usePriorityValue('modal', props, state);
const confirmLoading = usePriorityValue('confirmLoading', props, state);
const cancelText = usePriorityValue('cancelText', props, state);
const confirmText = usePriorityValue('confirmText', props, state);
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
const showCancelButton = usePriorityValue('showCancelButton', props, state);
const showConfirmButton = usePriorityValue('showConfirmButton', props, state);
const {
cancelText,
closable,
closeOnClickModal,
closeOnPressEscape,
confirmLoading,
confirmText,
description,
footer: showFooter,
loading: showLoading,
modal,
showCancelButton,
showConfirmButton,
title,
titleTooltip,
} = usePriorityValues(props, state);
watch(
() => showLoading.value,
@@ -116,12 +118,9 @@ function pointerDownOutside(e: Event) {
<slot name="title">
{{ title }}
<VbenTooltip v-if="titleTooltip" side="right">
<template #trigger>
<Info class="inline-flex size-5 cursor-pointer pb-1" />
</template>
<VbenHelpTooltip v-if="titleTooltip" trigger-class="pb-1">
{{ titleTooltip }}
</VbenTooltip>
</VbenHelpTooltip>
</slot>
</SheetTitle>
<SheetDescription v-if="description" class="mt-1 text-xs">

View File

@@ -6,7 +6,7 @@ import type {
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
import { useStore } from '@vben-core/shared';
import { useStore } from '@vben-core/shared/store';
import VbenDrawer from './drawer.vue';
import { DrawerApi } from './drawer-api';

View File

@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ModalApi } from '../modal-api'; // 假设 ModalApi 位于同一目录
import type { ModalState } from '../modal';
vi.mock('@vben-core/shared', () => {
vi.mock('@vben-core/shared/store', () => {
return {
isFunction: (fn: any) => typeof fn === 'function',
Store: class {

View File

@@ -1,6 +1,7 @@
import type { ModalApiOptions, ModalState } from './modal';
import { isFunction, Store } from '@vben-core/shared';
import { Store } from '@vben-core/shared/store';
import { bindMethods, isFunction } from '@vben-core/shared/utils';
export class ModalApi {
private api: Pick<
@@ -65,12 +66,15 @@ export class ModalApi {
},
);
this.state = this.store.state;
this.api = {
onBeforeClose,
onCancel,
onConfirm,
onOpenChange,
};
bindMethods(this);
}
// 如果需要多次更新状态,可以使用 batch 方法

View File

@@ -5,10 +5,10 @@ import { computed, nextTick, ref, watch } from 'vue';
import {
useIsMobile,
usePriorityValue,
usePriorityValues,
useSimpleLocale,
} from '@vben-core/composables';
import { Expand, Info, Shrink } from '@vben-core/icons';
import { Expand, Shrink } from '@vben-core/icons';
import {
Dialog,
DialogContent,
@@ -17,12 +17,12 @@ import {
DialogHeader,
DialogTitle,
VbenButton,
VbenHelpTooltip,
VbenIconButton,
VbenLoading,
VbenTooltip,
VisuallyHidden,
} from '@vben-core/shadcn-ui';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { useModalDraggable } from './use-modal-draggable';
@@ -52,25 +52,27 @@ const { $t } = useSimpleLocale();
const { isMobile } = useIsMobile();
const state = props.modalApi?.useStore?.();
const header = usePriorityValue('header', props, state);
const title = usePriorityValue('title', props, state);
const fullscreen = usePriorityValue('fullscreen', props, state);
const description = usePriorityValue('description', props, state);
const titleTooltip = usePriorityValue('titleTooltip', props, state);
const showFooter = usePriorityValue('footer', props, state);
const showLoading = usePriorityValue('loading', props, state);
const closable = usePriorityValue('closable', props, state);
const modal = usePriorityValue('modal', props, state);
const centered = usePriorityValue('centered', props, state);
const confirmLoading = usePriorityValue('confirmLoading', props, state);
const cancelText = usePriorityValue('cancelText', props, state);
const confirmText = usePriorityValue('confirmText', props, state);
const draggable = usePriorityValue('draggable', props, state);
const fullscreenButton = usePriorityValue('fullscreenButton', props, state);
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
const showCancelButton = usePriorityValue('showCancelButton', props, state);
const showConfirmButton = usePriorityValue('showConfirmButton', props, state);
const {
cancelText,
centered,
closable,
closeOnClickModal,
closeOnPressEscape,
confirmLoading,
confirmText,
description,
draggable,
footer: showFooter,
fullscreen,
fullscreenButton,
header,
loading: showLoading,
modal,
showCancelButton,
showConfirmButton,
title,
titleTooltip,
} = usePriorityValues(props, state);
const shouldFullscreen = computed(
() => (fullscreen.value && header.value) || isMobile.value,
@@ -184,12 +186,9 @@ function pointerDownOutside(e: Event) {
{{ title }}
<slot v-if="titleTooltip" name="titleTooltip">
<VbenTooltip side="right">
<template #trigger>
<Info class="inline-flex size-5 cursor-pointer pb-1" />
</template>
<VbenHelpTooltip trigger-class="pb-1">
{{ titleTooltip }}
</VbenTooltip>
</VbenHelpTooltip>
</slot>
</slot>
</DialogTitle>

View File

@@ -2,7 +2,7 @@ import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
import { useStore } from '@vben-core/shared';
import { useStore } from '@vben-core/shared/store';
import VbenModal from './modal.vue';
import { ModalApi } from './modal-api';
@@ -33,7 +33,15 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
...attrs,
...slots,
});
return () => h(connectedComponent, { ...props, ...attrs }, slots);
return () =>
h(
connectedComponent,
{
...props,
...attrs,
},
slots,
);
},
{
inheritAttrs: false,
@@ -65,7 +73,15 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
const Modal = defineComponent(
(props: ModalProps, { attrs, slots }) => {
return () =>
h(VbenModal, { ...props, ...attrs, modalApi: extendedApi }, slots);
h(
VbenModal,
{
...props,
...attrs,
modalApi: extendedApi,
},
slots,
);
},
{
inheritAttrs: false,

View File

@@ -11,6 +11,6 @@
"framework": "vite",
"aliases": {
"components": "@vben-core/shadcn-ui/components",
"utils": "@vben-core/shared"
"utils": "@vben-core/shared/utils"
}
}

View File

@@ -11,8 +11,8 @@
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"prepublishOnly": "npm run build"
"#build": "pnpm unbuild",
"#prepublishOnly": "npm run build"
},
"files": [
"dist"
@@ -20,24 +20,22 @@
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"#main": "./dist/index.mjs",
"main": "./src/index.ts",
"#module": "./dist/index.mjs",
"module": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
},
"./*": {
"types": "./src/*/index.ts",
"development": "./src/*/index.ts",
"default": "./dist/*/index.mjs"
"//default": "./dist/index.mjs",
"default": "./src/index.ts"
}
},
"publishConfig": {
"exports": {
".": {
"default": "./dist/index.mjs"
"default": "./src/index.ts"
}
}
},
@@ -50,6 +48,7 @@
"class-variance-authority": "^0.7.0",
"lucide-vue-next": "^0.439.0",
"radix-vue": "^1.9.5",
"vee-validate": "^4.13.2",
"vue": "^3.5.3"
}
}

View File

@@ -0,0 +1,24 @@
import type { AsTag } from 'radix-vue';
import type { ButtonVariants, ButtonVariantSize } from '../ui/button';
import type { Component } from 'vue';
export interface VbenButtonProps {
/**
* The element or component this component should render as. Can be overwrite by `asChild`
* @defaultValue "div"
*/
as?: AsTag | Component;
/**
* Change the default rendered element for the one passed as a child, merging their props and behavior.
*
* Read our [Composition](https://www.radix-vue.com/guides/composition.html) guide for more details.
*/
asChild?: boolean;
class?: any;
disabled?: boolean;
loading?: boolean;
size?: ButtonVariantSize;
variant?: ButtonVariants;
}

View File

@@ -1,20 +1,16 @@
<script setup lang="ts">
import type { VbenButtonProps } from './button';
import { computed } from 'vue';
import { LoaderCircle } from '@vben-core/icons';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import { Primitive } from 'radix-vue';
import { type ButtonVariants, buttonVariants } from '../ui/button';
import { buttonVariants } from '../ui/button';
interface Props extends PrimitiveProps {
class?: any;
disabled?: boolean;
loading?: boolean;
size?: ButtonVariants['size'];
variant?: ButtonVariants['variant'];
}
interface Props extends VbenButtonProps {}
const props = withDefaults(defineProps<Props>(), {
as: 'button',

View File

@@ -1,22 +1,21 @@
<script setup lang="ts">
import type { ButtonVariants } from '../ui/button';
import type { VbenButtonProps } from './button';
import { computed, useSlots } from 'vue';
import { cn } from '@vben-core/shared';
import { type PrimitiveProps } from 'radix-vue';
import { cn } from '@vben-core/shared/utils';
import { VbenTooltip } from '../tooltip';
import VbenButton from './button.vue';
interface Props extends PrimitiveProps {
interface Props extends VbenButtonProps {
class?: any;
disabled?: boolean;
onClick?: () => void;
tooltip?: string;
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
variant?: ButtonVariants['variant'];
variant?: ButtonVariants;
}
const props = withDefaults(defineProps<Props>(), {

View File

@@ -1,2 +1,3 @@
export type * from './button';
export { default as VbenButton } from './button.vue';
export { default as VbenIconButton } from './icon-button.vue';

View File

@@ -1,24 +1,26 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';
import { useId } from 'vue';
import { useForwardPropsEmits } from 'radix-vue';
import { Checkbox } from '../ui/checkbox';
const props = defineProps<
{
name: string;
} & CheckboxRootProps
>();
const props = defineProps<CheckboxRootProps>();
const emits = defineEmits<CheckboxRootEmits>();
const checked = defineModel<boolean>('checked');
const forwarded = useForwardPropsEmits(props, emits);
const id = useId();
</script>
<template>
<Checkbox v-bind="forwarded" :id="name" v-model:checked="checked" />
<label :for="name" class="ml-2 cursor-pointer text-sm"> <slot></slot> </label>
<div class="flex items-center">
<Checkbox v-bind="forwarded" :id="id" v-model:checked="checked" />
<label :for="id" class="ml-2 cursor-pointer text-sm"> <slot></slot> </label>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { computed, onMounted, ref, unref, watch, watchEffect } from 'vue';
import { isNumber } from '@vben-core/shared';
import { isNumber } from '@vben-core/shared/utils';
import { TransitionPresets, useTransition } from '@vueuse/core';

View File

@@ -0,0 +1,36 @@
<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(
'text-primary hover:text-primary-hover inline-flex cursor-pointer 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

@@ -22,7 +22,7 @@ isFullscreen.value = !!(
</script>
<template>
<VbenIconButton @click="toggle">
<Minimize v-if="isFullscreen" class="size-4" />
<Maximize v-else class="size-4" />
<Minimize v-if="isFullscreen" class="text-foreground size-4" />
<Maximize v-else class="text-foreground size-4" />
</VbenIconButton>
</template>

View File

@@ -2,7 +2,12 @@
import { type Component, computed } from 'vue';
import { Icon, IconDefault } from '@vben-core/icons';
import { isFunction, isHttpUrl, isObject, isString } from '@vben-core/shared';
import {
isFunction,
isHttpUrl,
isObject,
isString,
} from '@vben-core/shared/utils';
const props = defineProps<{
// 没有是否显示默认图标

View File

@@ -6,10 +6,10 @@ 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';
export * from './input-password';
export * from './link';
export * from './logo';
@@ -19,9 +19,11 @@ export * from './popover';
export * from './render-content';
export * from './scrollbar';
export * from './segmented';
export * from './select';
export * from './spinner';
export * from './swap';
export * from './tooltip';
export * from './ui/accordion';
export * from './ui/avatar';
export * from './ui/badge';
export * from './ui/breadcrumb';
@@ -30,16 +32,21 @@ export * from './ui/card';
export * from './ui/checkbox';
export * from './ui/dialog';
export * from './ui/dropdown-menu';
export * from './ui/form';
export * from './ui/hover-card';
export * from './ui/input';
export * from './ui/label';
export * from './ui/number-field';
export * from './ui/pin-input';
export * from './ui/popover';
export * from './ui/radio-group';
export * from './ui/scroll-area';
export * from './ui/select';
export * from './ui/separator';
export * from './ui/sheet';
export * from './ui/switch';
export * from './ui/tabs';
export * from './ui/textarea';
export * from './ui/toast';
export * from './ui/toggle';
export * from './ui/toggle-group';

View File

@@ -2,13 +2,18 @@
import { ref, useSlots } from 'vue';
import { Eye, EyeOff } from '@vben-core/icons';
import { cn } from '@vben-core/shared/utils';
import { useForwardProps } from 'radix-vue';
import { type InputProps, VbenInput } from '../input';
import { Input } from '../ui/input';
import PasswordStrength from './password-strength.vue';
interface Props extends InputProps {}
interface Props {
class?: any;
/**
* 是否显示密码强度
*/
passwordStrength?: boolean;
}
defineOptions({
inheritAttrs: false,
@@ -19,30 +24,30 @@ const props = defineProps<Props>();
const modelValue = defineModel<string>();
const slots = useSlots();
const forward = useForwardProps(props);
const show = ref(false);
</script>
<template>
<div class="relative">
<VbenInput
<div class="relative w-full">
<Input
v-bind="$attrs"
v-model="modelValue"
v-bind="{ ...forward, ...$attrs }"
:class="cn(props.class)"
:type="show ? 'text' : 'password'"
>
<template v-if="passwordStrength">
<PasswordStrength :password="modelValue" />
<p
v-if="slots.strengthText"
class="text-muted-foreground mt-1.5 text-xs"
>
<slot name="strengthText"> </slot>
</p>
</template>
</VbenInput>
/>
<template v-if="passwordStrength">
<PasswordStrength :password="modelValue" />
<p v-if="slots.strengthText" class="text-muted-foreground mt-1.5 text-xs">
<slot name="strengthText"> </slot>
</p>
</template>
<div
class="hover:text-foreground text-foreground/60 absolute inset-y-0 right-0 top-3 flex cursor-pointer pr-3 text-lg leading-5"
:class="{
'top-3': !!passwordStrength,
'top-1/2 -translate-y-1/2 items-center': !passwordStrength,
}"
class="hover:text-foreground text-foreground/60 absolute inset-y-0 right-0 flex cursor-pointer pr-3 text-lg leading-5"
@click="show = !show"
>
<Eye v-if="show" class="size-4" />

View File

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

View File

@@ -1,53 +0,0 @@
<script setup lang="ts">
import type { InputProps } from './types';
import { computed } from 'vue';
defineOptions({
inheritAttrs: false,
});
const props = defineProps<InputProps>();
const modelValue = defineModel<number | string>();
const inputClass = computed(() => {
if (props.status === 'error') {
return 'border-destructive';
}
return '';
});
</script>
<template>
<div class="relative mb-6">
<label
v-if="!label"
:for="name"
class="mb-2 block text-sm font-medium dark:text-white"
>
{{ label }}
</label>
<input
:id="name"
v-model="modelValue"
:class="[props.class, inputClass]"
autocomplete="off"
class="border-input bg-input-background ring-offset-background placeholder:text-muted-foreground/60 focus-visible:ring-ring focus:border-primary flex h-10 w-full rounded-md border p-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
required
type="text"
v-bind="$attrs"
/>
<slot></slot>
<Transition name="slide-up">
<p
v-if="status === 'error'"
class="text-destructive bottom-130 absolute mt-1 text-xs"
>
{{ errorTip }}
</p>
</Transition>
</div>
</template>

View File

@@ -1,25 +0,0 @@
interface InputProps {
class?: any;
/**
* 错误提示信息
*/
errorTip?: string;
/**
* 输入框的 label
*/
label?: string;
/**
* 输入框的 name
*/
name?: string;
/**
* 是否显示密码强度
*/
passwordStrength?: boolean;
/**
* 输入框的校验状态
*/
status?: 'default' | 'error';
}
export type { InputProps };

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { Primitive, type PrimitiveProps } from 'radix-vue';

View File

@@ -3,7 +3,7 @@ import type { MenuRecordBadgeRaw } from '@vben-core/typings';
import { computed } from 'vue';
import { isValidColor } from '@vben-core/shared';
import { isValidColor } from '@vben-core/shared/color';
import BadgeDot from './menu-badge-dot.vue';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { PinInputProps } from './types';
import { computed, ref, watch } from 'vue';
import { computed, onBeforeUnmount, ref, useId, watch } from 'vue';
import { VbenButton } from '../button';
import { PinInput, PinInputGroup, PinInputInput } from '../ui/pin-input';
@@ -14,21 +14,27 @@ const props = withDefaults(defineProps<PinInputProps>(), {
btnLoading: false,
codeLength: 6,
handleSendCode: async () => {},
maxTime: 60,
});
const emit = defineEmits<{
complete: [];
}>();
const timer = ref<ReturnType<typeof setTimeout>>();
const modelValue = defineModel<string>();
const inputValue = ref<string[]>([]);
const countdown = ref(0);
const inputClass = computed(() => {
if (props.status === 'error') {
return 'border-destructive';
}
return '';
const btnText = computed(() => {
const countdownValue = countdown.value;
return props.createText?.(countdownValue);
});
const btnLoading = computed(() => {
return props.loading || countdown.value > 0;
});
watch(
@@ -42,45 +48,58 @@ function handleComplete(e: string[]) {
modelValue.value = e.join('');
emit('complete');
}
async function handleSend(e: Event) {
e?.preventDefault();
await props.handleSendCode();
countdown.value = props.maxTime;
startCountdown();
}
function startCountdown() {
if (countdown.value > 0) {
timer.value = setTimeout(() => {
countdown.value--;
startCountdown();
}, 1000);
}
}
onBeforeUnmount(() => {
countdown.value = 0;
clearTimeout(timer.value);
});
const id = useId();
</script>
<template>
<div class="relative mb-6">
<label :for="name" class="mb-2 block text-sm font-medium">
{{ label }}
</label>
<PinInput
:id="name"
v-model="inputValue"
:class="inputClass"
class="flex justify-between"
otp
placeholder="○"
type="number"
@complete="handleComplete"
>
<PinInputGroup>
<PinInput
:id="id"
v-model="inputValue"
class="flex w-full justify-between"
otp
placeholder=""
type="number"
@complete="handleComplete"
>
<div class="relative flex w-full">
<PinInputGroup class="mr-2">
<PinInputInput
v-for="(id, index) in codeLength"
:key="id"
v-for="(item, index) in codeLength"
:key="item"
:index="index"
/>
</PinInputGroup>
<VbenButton
:loading="btnLoading"
class="w-[300px] xl:w-full"
class="flex-grow"
size="lg"
variant="outline"
@click="handleSendCode"
@click="handleSend"
>
{{ btnText }}
</VbenButton>
</PinInput>
<p
v-if="status === 'error'"
class="text-destructive bottom-130 absolute mt-1 text-xs"
>
{{ errorTip }}
</p>
</div>
</div>
</PinInput>
</template>

View File

@@ -1,38 +1,26 @@
interface PinInputProps {
/**
* 发送验证码按钮loading
*/
btnLoading?: boolean;
/**
* 发送验证码按钮文本
*/
btnText?: string;
class?: any;
/**
* 验证码长度
*/
codeLength?: number;
/**
* 错误提示信息
* 发送验证码按钮文本
*/
errorTip?: string;
createText?: (countdown: number) => string;
/**
* 自定义验证码发送逻辑
* @returns
*/
handleSendCode?: () => Promise<void>;
/**
* 输入框的 label
* 发送验证码按钮loading
*/
label: string;
loading?: boolean;
/**
* 输入框的 name
* 最大重试时间
*/
name: string;
/**
* 输入框的校验状态
*/
status?: 'default' | 'error';
maxTime?: number;
}
export type { PinInputProps };

View File

@@ -1,26 +1,39 @@
<script setup lang="ts">
import type { Component } from 'vue';
<script lang="ts">
import type { Component, PropType } from 'vue';
import { defineComponent, h } from 'vue';
defineOptions({
import { isFunction, isObject } from '@vben-core/shared/utils';
export default defineComponent({
name: 'RenderContent',
});
const props = withDefaults(
defineProps<{
content: Component | string | undefined;
props?: Record<string, any>;
}>(),
{
props: () => ({}),
props: {
content: {
default: undefined as
| PropType<(() => any) | Component | string>
| undefined,
type: [Object, String, Function],
},
},
);
const isComponent = typeof props.content === 'object' && props.content !== null;
setup(props, { attrs, slots }) {
return () => {
if (!props.content) {
return null;
}
const isComponent =
(isObject(props.content) || isFunction(props.content)) &&
props.content !== null;
if (!isComponent) {
return props.content;
}
return h(props.content as never, {
...attrs,
props: {
...props,
...attrs,
},
slots,
});
};
},
});
</script>
<template>
<component :is="content" v-bind="props" v-if="isComponent" />
<template v-else-if="!isComponent">
{{ content }}
</template>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { ScrollArea, ScrollBar } from '../ui/scroll-area';

View File

@@ -1,13 +1,11 @@
<script setup lang="ts">
import type { TabsIndicatorProps } from 'radix-vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import {
TabsIndicator,
type TabsIndicatorProps,
useForwardProps,
} from 'radix-vue';
import { TabsIndicator, useForwardProps } from 'radix-vue';
const props = defineProps<{ class?: any } & TabsIndicatorProps>();

View File

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

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select';
interface Props {
class?: any;
options?: Array<{ label: string; value: string }>;
placeholder?: string;
}
const props = defineProps<Props>();
</script>
<template>
<Select>
<SelectTrigger :class="props.class">
<SelectValue :placeholder="placeholder" />
</SelectTrigger>
<SelectContent>
<template v-for="item in options" :key="item.value">
<SelectItem :value="item.value"> {{ item.label }} </SelectItem>
</template>
</SelectContent>
</Select>
</template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
interface Props {
class?: string;

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
interface Props {
class?: string;

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
import { CircleHelp } from 'lucide-vue-next';
import Tooltip from './tooltip.vue';
defineOptions({
inheritAttrs: false,
});
defineProps<{ triggerClass?: string }>();
</script>
<template>
<Tooltip :delay-duration="300" side="right">
<template #trigger>
<slot name="trigger">
<CircleHelp
:class="
cn(
'text-foreground/80 hover:text-foreground inline-flex size-5 cursor-pointer',
triggerClass,
)
"
/>
</slot>
</template>
<slot></slot>
</Tooltip>
</template>

View File

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

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
AccordionRoot,
type AccordionRootEmits,
type AccordionRootProps,
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<AccordionRootProps>();
const emits = defineEmits<AccordionRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot></slot>
</AccordionRoot>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { AccordionContent, type AccordionContentProps } from 'radix-vue';
const props = defineProps<{ class?: any } & AccordionContentProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot></slot>
</div>
</AccordionContent>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import {
AccordionItem,
type AccordionItemProps,
useForwardProps,
} from 'radix-vue';
const props = defineProps<{ class?: any } & AccordionItemProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
<slot></slot>
</AccordionItem>
</template>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { ChevronDownIcon } from '@radix-icons/vue';
import {
AccordionHeader,
AccordionTrigger,
type AccordionTriggerProps,
} from 'radix-vue';
const props = defineProps<{ class?: any } & AccordionTriggerProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class,
)
"
>
<slot></slot>
<slot name="icon">
<ChevronDownIcon
class="text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>

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

@@ -1,7 +1,5 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { AvatarRoot } from 'radix-vue';
@@ -9,7 +7,7 @@ import { avatarVariant, type AvatarVariants } from './avatar';
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class'];
class?: any;
shape?: AvatarVariants['shape'];
size?: AvatarVariants['size'];
}>(),

View File

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

View File

@@ -1,8 +1,6 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,12 +1,10 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { DotsHorizontalIcon } from '@radix-icons/vue';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,10 +1,8 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,16 +1,11 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { Primitive, type PrimitiveProps } from 'radix-vue';
const props = withDefaults(
defineProps<{ class?: HTMLAttributes['class'] } & PrimitiveProps>(),
{
as: 'a',
},
);
const props = withDefaults(defineProps<{ class?: any } & PrimitiveProps>(), {
as: 'a',
});
</script>
<template>

View File

@@ -1,10 +1,8 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,10 +1,8 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,12 +1,10 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { ChevronRightIcon } from '@radix-icons/vue';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import type { ButtonVariants, ButtonVariantSize } from './types';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import { type ButtonVariants, buttonVariants } from './button';
import { buttonVariants } from './button';
interface Props extends PrimitiveProps {
class?: HTMLAttributes['class'];
size?: ButtonVariants['size'];
variant?: 'heavy' & ButtonVariants['variant'];
class?: any;
size?: ButtonVariantSize;
variant?: 'heavy' & ButtonVariants;
}
const props = withDefaults(defineProps<Props>(), {

View File

@@ -1,4 +1,4 @@
import { cva, type VariantProps } from 'class-variance-authority';
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',
@@ -32,5 +32,3 @@ export const buttonVariants = cva(
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

@@ -1,3 +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

@@ -1,10 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { CheckIcon } from '@radix-icons/vue';
import {
@@ -12,9 +12,7 @@ import {
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & CheckboxRootProps
>();
const props = defineProps<{ class?: any } & CheckboxRootProps>();
const emits = defineEmits<CheckboxRootEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { CheckIcon } from '@radix-icons/vue';
import {
@@ -12,9 +12,7 @@ import {
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & ContextMenuCheckboxItemProps
>();
const props = defineProps<{ class?: any } & ContextMenuCheckboxItemProps>();
const emits = defineEmits<ContextMenuCheckboxItemEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import {
ContextMenuContent,
@@ -11,9 +11,7 @@ import {
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & ContextMenuContentProps
>();
const props = defineProps<{ class?: any } & ContextMenuContentProps>();
const emits = defineEmits<ContextMenuContentEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import {
ContextMenuItem,
@@ -11,7 +11,7 @@ import {
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class']; inset?: boolean } & ContextMenuItemProps
{ class?: any; inset?: boolean } & ContextMenuItemProps
>();
const emits = defineEmits<ContextMenuItemEmits>();

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { ContextMenuLabel, type ContextMenuLabelProps } from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class']; inset?: boolean } & ContextMenuLabelProps
{ class?: any; inset?: boolean } & ContextMenuLabelProps
>();
const delegatedProps = computed(() => {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { DotFilledIcon } from '@radix-icons/vue';
import {
@@ -12,9 +12,7 @@ import {
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & ContextMenuRadioItemProps
>();
const props = defineProps<{ class?: any } & ContextMenuRadioItemProps>();
const emits = defineEmits<ContextMenuRadioItemEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,16 +1,14 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import {
ContextMenuSeparator,
type ContextMenuSeparatorProps,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & ContextMenuSeparatorProps
>();
const props = defineProps<{ class?: any } & ContextMenuSeparatorProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import {
ContextMenuSubContent,
@@ -10,9 +10,7 @@ import {
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & DropdownMenuSubContentProps
>();
const props = defineProps<{ class?: any } & DropdownMenuSubContentProps>();
const emits = defineEmits<DropdownMenuSubContentEmits>();
const delegatedProps = computed(() => {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { ChevronRightIcon } from '@radix-icons/vue';
import {
@@ -12,7 +12,7 @@ import {
const props = defineProps<
{
class?: HTMLAttributes['class'];
class?: any;
inset?: boolean;
} & ContextMenuSubTriggerProps
>();

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import { Cross2Icon } from '@radix-icons/vue';
import {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, type HTMLAttributes } from 'vue';
import { computed } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
import {
DialogDescription,
@@ -9,9 +9,7 @@ import {
useForwardProps,
} from 'radix-vue';
const props = defineProps<
{ class?: HTMLAttributes['class'] } & DialogDescriptionProps
>();
const props = defineProps<{ class?: any } & DialogDescriptionProps>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { cn } from '@vben-core/shared';
const props = defineProps<{ class?: HTMLAttributes['class'] }>();
const props = defineProps<{ class?: any }>();
</script>
<template>

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@vben-core/shared';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: HTMLAttributes['class'];
class?: any;
}>();
</script>

Some files were not shown because too many files have changed in this diff Show More