feat: 优化表格组件
自动部署 / build (push) Successful in 1m31s Details

master
luoer 2023-11-21 17:19:48 +08:00
parent db2c3005e8
commit c8dc40127a
75 changed files with 866 additions and 2199 deletions

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ import { useFormItems } from './useFormItems';
import { useFormModel } from './useFormModel'; import { useFormModel } from './useFormModel';
import { useFormRef } from './useFormRef'; import { useFormRef } from './useFormRef';
import { useFormSubmit } from './useFormSubmit'; import { useFormSubmit } from './useFormSubmit';
import { AnFormItem, IAnFormItem } from './FormItem'; import { AnFormItem, AnFormItemProps } from './FormItem';
/** /**
* *
@ -29,9 +29,17 @@ export const AnForm = defineComponent({
}, },
/** /**
* *
* @example
* ```ts
* [{
* field: 'name',
* label: '昵称',
* setter: 'input'
* }]
* ```
*/ */
items: { items: {
type: Array as PropType<IAnFormItem[]>, type: Array as PropType<AnFormItemProps[]>,
default: () => [], default: () => [],
}, },
/** /**
@ -42,7 +50,7 @@ export const AnForm = defineComponent({
* ``` * ```
*/ */
submit: { submit: {
type: [String, Function, Object] as PropType<IAnFormSubmit>, type: [String, Function, Object] as PropType<AnFormSubmit>,
}, },
/** /**
* Form * Form
@ -54,7 +62,7 @@ export const AnForm = defineComponent({
* ``` * ```
*/ */
formProps: { formProps: {
type: Object as IAnFormProps, type: Object as PropType<Omit<FormInstance['$props'], 'model' | 'ref'>>,
}, },
}, },
emits: ['update:model'], emits: ['update:model'],
@ -87,12 +95,8 @@ export const AnForm = defineComponent({
export type AnFormInstance = InstanceType<typeof AnForm>; export type AnFormInstance = InstanceType<typeof AnForm>;
export type AnFormProps = AnFormInstance['$props']; export type AnFormProps = Pick<AnFormInstance['$props'], 'model' | 'items' | 'submit' | 'formProps'>;
export type IAnFormProps = PropType<Omit<FormInstance['$props'], 'model'>>; export type AnFormSubmitFn = (model: Recordable, items: AnFormItemProps[]) => any;
export type IAnForm = Pick<AnFormProps, 'model' | 'items' | 'submit' | 'formProps'>; export type AnFormSubmit = string | AnFormSubmitFn;
export type IAnFormSubmitFn = (model: Recordable, items: IAnFormItem[]) => any;
export type IAnFormSubmit = string | IAnFormSubmitFn;

View File

@ -8,7 +8,7 @@ import {
import { InjectionKey, PropType, provide } from 'vue'; import { InjectionKey, PropType, provide } from 'vue';
import { SetterItem, SetterType, setterMap } from './FormSetter'; import { SetterItem, SetterType, setterMap } from './FormSetter';
export const FormItemContextKey = Symbol('FormItemContextKey') as InjectionKey<IAnFormItemFnProps>; export const FormItemContextKey = Symbol('FormItemContextKey') as InjectionKey<AnFormItemFnProps>;
/** /**
* *
@ -20,14 +20,14 @@ export const AnFormItem = defineComponent({
* *
*/ */
item: { item: {
type: Object as PropType<IAnFormItem>, type: Object as PropType<AnFormItemProps>,
required: true, required: true,
}, },
/** /**
* *
*/ */
items: { items: {
type: Array as PropType<IAnFormItem[]>, type: Array as PropType<AnFormItemProps[]>,
required: true, required: true,
}, },
/** /**
@ -101,46 +101,45 @@ export const AnFormItem = defineComponent({
}, },
}); });
export type IAnFormItemBoolFn = (args: IAnFormItemFnProps) => boolean; export type AnFormItemBoolFn = (args: AnFormItemFnProps) => boolean;
export type IAnFormItemElemFn = (args: IAnFormItemFnProps) => any; export type AnFormItemElemFn = (args: AnFormItemFnProps) => any;
export type IAnFormItemFnProps = { model: Recordable; item: IAnFormItem; items: IAnFormItem[] }; export type AnFormItemFnProps = { model: Recordable; item: AnFormItemProps; items: AnFormItemProps[] };
export type IAnFormItemRule = FieldRule & { disable?: IAnFormItemBoolFn }; export type AnFormItemRule = FieldRule & { disable?: AnFormItemBoolFn };
export type IAnFormItemOption = string | number | boolean | SelectOptionData | SelectOptionGroup; export type AnFormItemOption = string | number | boolean | SelectOptionData | SelectOptionGroup;
export type IAnFormItemSlot = (props: IAnFormItemFnProps) => any; export type AnFormItemSlot = (props: AnFormItemFnProps) => any;
export type IAnFormItemSlots = { export type AnFormItemSlots = {
/** /**
* *
* @param props * @param props
* @returns
*/ */
default?: IAnFormItemSlot; default?: AnFormItemSlot;
/** /**
* *
* @param props * @param props
* @returns
*/ */
help?: IAnFormItemSlot; help?: AnFormItemSlot;
/** /**
* *
* @param props * @param props
* @returns
*/ */
extra?: IAnFormItemSlot; extra?: AnFormItemSlot;
/** /**
* *
* @param props * @param props
* @returns
*/ */
label?: IAnFormItemSlot; label?: AnFormItemSlot;
}; };
export type IAnFormItemBase = { export type AnFormItemPropsBase = {
/** /**
* *
* @description * @description
@ -162,9 +161,12 @@ export type IAnFormItemBase = {
/** /**
* *
* @example ['email'] * @example
* ```ts
* ['email']
* ```
*/ */
rules?: IAnFormItemRule[]; rules?: AnFormItemRule[];
/** /**
* *
@ -173,7 +175,7 @@ export type IAnFormItemBase = {
* (props) => Boolean(props.model.id) * (props) => Boolean(props.model.id)
* ``` * ```
*/ */
visible?: IAnFormItemBoolFn; visible?: AnFormItemBoolFn;
/** /**
* *
@ -182,29 +184,42 @@ export type IAnFormItemBase = {
* (props) => Boolean(props.model.id) * (props) => Boolean(props.model.id)
* ``` * ```
*/ */
disable?: IAnFormItemBoolFn; disable?: AnFormItemBoolFn;
/** /**
* *
* @description * @description
* @example * @example
* ```ts * ```ts
* [{ label: '方式1', value: 1 }] * [{
* label: '方式1',
* value: 1
* }]
* ``` * ```
*/ */
options?: IAnFormItemOption[] | ((args: IAnFormItemFnProps) => IAnFormItemOption[] | Promise<IAnFormItemOption[]>); options?: AnFormItemOption[] | ((args: AnFormItemFnProps) => AnFormItemOption[] | Promise<AnFormItemOption[]>);
/** /**
* *
* @default null * @example
* ```ts
* {
* hideLabel: true
* }
* ```
*/ */
itemProps?: Partial<Omit<FormItemInstance['$props'], 'field' | 'label' | 'required' | 'rules' | 'disabled'>>; itemProps?: Partial<Omit<FormItemInstance['$props'], 'field' | 'label' | 'required' | 'rules' | 'disabled'>>;
/** /**
* *
* @see 1 * @example
* ```tsx
* {
* help: () => <span></span>
* }
* ```
*/ */
itemSlots?: IAnFormItemSlots; itemSlots?: AnFormItemSlots;
/** /**
* *
@ -213,4 +228,4 @@ export type IAnFormItemBase = {
$init?: () => void; $init?: () => void;
}; };
export type IAnFormItem = IAnFormItemBase & SetterItem; export type AnFormItemProps = AnFormItemPropsBase & SetterItem;

View File

@ -1,41 +1,73 @@
import { useVisible } from "@/hooks/useVisible"; import { useVisible } from '@/hooks/useVisible';
import { Button, ButtonInstance, Modal } from "@arco-design/web-vue"; import { Button, ButtonInstance, FormInstance, Modal } from '@arco-design/web-vue';
import { PropType } from "vue"; import { InjectionKey, PropType, Ref } from 'vue';
import { useModalSubmit } from "./useModalSubmit"; import { useModalSubmit } from './useModalSubmit';
import { useModalTrigger } from "./useModalTrigger"; import { useModalTrigger } from './useModalTrigger';
import { AnForm, IAnFormProps, IAnFormSubmit } from "./Form"; import { AnForm, AnFormInstance, AnFormProps, AnFormSubmit } from './Form';
import { IAnFormItem } from "./FormItem"; import { AnFormItemProps } from './FormItem';
export interface AnFormModalContext {
visible: Ref<boolean>;
loading: Ref<boolean>;
formRef: Ref<AnFormInstance | null>;
open: (data: Recordable) => void;
close: () => void;
submitForm: () => any | Promise<any>;
modalTitle: () => any;
modalTrigger: () => any;
onClose: () => void;
}
export const AnFormModalContextKey = Symbol('AnFormModalContextKey') as InjectionKey<AnFormModalContext>;
/** /**
* *
*/ */
export const AnFormModal = defineComponent({ export const AnFormModal = defineComponent({
name: "AnFormModal", name: 'AnFormModal',
props: { props: {
/** /**
* *
* @default '新增' * @default
* ```ts
* '新增'
* ```
*/ */
title: { title: {
type: [String, Function] as PropType<ModalType>, type: [String, Function] as PropType<AnFormModalTitle>,
default: "新增", default: '新增',
}, },
/** /**
* *
* @default '新增' * @default
* ```ts
* '新增'
* ```
*/ */
trigger: { trigger: {
type: [Boolean, String, Function, Object] as PropType<ModalTrigger>, type: [Boolean, String, Function, Object] as PropType<AnFormModalTrigger>,
default: true, default: true,
}, },
/** /**
* Modalprops * Modalprops
* @example
* ```ts
* {
* closable: true
* }
* ```
*/ */
modalProps: { modalProps: {
type: Object as PropType<ModalProps>, type: Object as PropType<Omit<InstanceType<typeof Modal>['$props'], 'visible' | 'title'>>,
}, },
/** /**
* *
* @example
* ```ts
* {
* id: undefined
* }
* ```
*/ */
model: { model: {
type: Object as PropType<Recordable>, type: Object as PropType<Recordable>,
@ -43,27 +75,45 @@ export const AnFormModal = defineComponent({
}, },
/** /**
* *
* @example
* ```ts
* [{
* field: 'name',
* label: '昵称',
* setter: 'input'
* }]
* ```
*/ */
items: { items: {
type: Array as PropType<IAnFormItem[]>, type: Array as PropType<AnFormItemProps[]>,
default: () => [], default: () => [],
}, },
/** /**
* *
* @example
* ```ts
* (model) => api.user.addUser(model)
* ```
*/ */
submit: { submit: {
type: [String, Function] as PropType<IAnFormSubmit>, type: [String, Function] as PropType<AnFormSubmit>,
}, },
/** /**
* Form * Form
* @example
* ```ts
* {
* layout: 'vertical'
* }
* ```
*/ */
formProps: { formProps: {
type: Object as IAnFormProps, type: Object as PropType<Omit<FormInstance['$props'], 'model' | 'ref'>>,
}, },
}, },
emits: ["update:model"], emits: ['update:model'],
setup(props, { slots, emit }) { setup(props) {
const formRef = ref<InstanceType<typeof AnForm> | null>(null); const formRef = ref<AnFormInstance | null>(null);
const { visible, show, hide } = useVisible(); const { visible, show, hide } = useVisible();
const { modalTrigger } = useModalTrigger(props, show); const { modalTrigger } = useModalTrigger(props, show);
const { loading, setLoading, submitForm } = useModalSubmit(props, formRef, visible); const { loading, setLoading, submitForm } = useModalSubmit(props, formRef, visible);
@ -81,13 +131,13 @@ export const AnFormModal = defineComponent({
const onClose = () => {}; const onClose = () => {};
const modalTitle = () => { const modalTitle = () => {
if (typeof props.title === "string") { if (typeof props.title === 'string') {
return props.title; return props.title;
} }
return <props.title model={props.model} items={props.items}></props.title>; return <props.title model={props.model} items={props.items}></props.title>;
}; };
return { const context: AnFormModalContext = {
visible, visible,
loading, loading,
formRef, formRef,
@ -98,6 +148,8 @@ export const AnFormModal = defineComponent({
modalTrigger, modalTrigger,
onClose, onClose,
}; };
return context;
}, },
render() { render() {
return ( return (
@ -118,7 +170,7 @@ export const AnFormModal = defineComponent({
<AnForm <AnForm
ref="formRef" ref="formRef"
model={this.model} model={this.model}
onUpdate:model={(v) => this.$emit("update:model", v)} onUpdate:model={v => this.$emit('update:model', v)}
items={this.items} items={this.items}
formProps={this.formProps} formProps={this.formProps}
></AnForm> ></AnForm>
@ -141,21 +193,21 @@ export const AnFormModal = defineComponent({
}, },
}); });
type ModalProps = Partial<Omit<InstanceType<typeof Modal>["$props"], "visible" | "title" | "onBeforeOk">>; export type AnFormModalTitle = string | ((model: Recordable, items: AnFormItemProps[]) => any);
type ModalType = string | ((model: Recordable, items: IAnFormItem[]) => any); export type AnFormModalTrigger =
type ModalTrigger =
| boolean | boolean
| string | string
| ((model: Recordable, items: IAnFormItem[]) => any) | ((model: Recordable, items: AnFormItemProps[]) => any)
| { | {
text?: string; text?: string;
buttonProps?: ButtonInstance["$props"]; buttonProps?: ButtonInstance['$props'];
buttonSlots?: Recordable; buttonSlots?: Recordable;
}; };
export type FormModalProps = Pick< export type AnFormModalInstance = InstanceType<typeof AnFormModal>;
InstanceType<typeof AnFormModal>["$props"],
"title" | "trigger" | "modalProps" | "model" | "items" | "submit" | "formProps" export type AnFormModalProps = Pick<
AnFormModalInstance['$props'],
'title' | 'trigger' | 'modalProps' | 'model' | 'items' | 'submit' | 'formProps'
>; >;

View File

@ -1,8 +1,8 @@
import { Ref } from 'vue'; import { Ref } from 'vue';
import { IAnFormItem } from './FormItem'; import { AnFormItemProps } from './FormItem';
import { setterMap } from './FormSetter'; import { setterMap } from './FormSetter';
export function useFormItems(items: Ref<IAnFormItem[]>, model: Ref<Recordable>) { export function useFormItems(items: Ref<AnFormItemProps[]>, model: Ref<Recordable>) {
const getItem = (field: string) => { const getItem = (field: string) => {
return items.value.find(i => i.field === field); return items.value.find(i => i.field === field);
}; };

View File

@ -1,6 +1,6 @@
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { IAnForm } from './Form'; import { AnFormProps } from './Form';
import { IAnFormItem } from './FormItem'; import { AnFormItemProps } from './FormItem';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
const SUBMIT_ITEM = { const SUBMIT_ITEM = {
@ -11,9 +11,9 @@ const SUBMIT_ITEM = {
}, },
}; };
export function useFormSubmit(props: IAnForm, validate: any, getModel: any) { export function useFormSubmit(props: AnFormProps, validate: any, getModel: any) {
const loading = ref(false); const loading = ref(false);
const submitItem = ref<IAnFormItem | null>(null); const submitItem = ref<AnFormItemProps | null>(null);
if (props.submit) { if (props.submit) {
submitItem.value = cloneDeep(SUBMIT_ITEM); submitItem.value = cloneDeep(SUBMIT_ITEM);

View File

@ -1,7 +1,7 @@
import { AnForm, IAnForm } from '../components/Form'; import { AnForm, AnFormInstance, AnFormProps } from '../components/Form';
import { FormItem, useItems } from './useItems'; import { FormItem, useItems } from './useItems';
export type FormUseOptions = Partial<Omit<IAnForm, 'items'>> & { export type FormUseOptions = Partial<Omit<AnFormProps, 'items'>> & {
/** /**
* *
* @example * @example
@ -16,25 +16,23 @@ export type FormUseOptions = Partial<Omit<IAnForm, 'items'>> & {
items?: FormItem[]; items?: FormItem[];
}; };
export function useFormProps(options: FormUseOptions) { export function useFormProps(options: FormUseOptions): Required<AnFormProps> {
const _model = options.model ?? {}; const { model = {}, items: _items = [], submit = () => null, formProps = {} } = options;
const _items = options.items ?? []; const items = useItems(_items ?? [], model);
const items = useItems(_items, _model); return {
const props = reactive({ model,
formProps: options.formProps ?? {}, items,
items: items.value, submit,
submit: options.submit, formProps,
model: _model, };
});
return props;
} }
/** /**
* *
*/ */
export const useForm = (options: FormUseOptions) => { export const useForm = (options: FormUseOptions) => {
const props = useFormProps(options); const props = reactive(useFormProps(options));
const formRef = ref<InstanceType<typeof AnForm> | null>(null); const formRef = ref<AnFormInstance | null>(null);
const AnFormer = () => ( const AnFormer = () => (
<AnForm <AnForm
@ -48,7 +46,7 @@ export const useForm = (options: FormUseOptions) => {
return { return {
component: AnFormer, component: AnFormer,
props,
formRef, formRef,
props,
}; };
}; };

View File

@ -1,28 +1,31 @@
import { AnFormModal, FormModalProps } from '../components/FormModal'; import { AnFormModal, AnFormModalProps } from '../components/FormModal';
import { useFormProps } from './useForm'; import { useFormProps } from './useForm';
import { FormItem } from './useItems'; import { FormItem } from './useItems';
export type FormModalUseOptions = Partial<Omit<FormModalProps, 'items'>> & { export type FormModalUseOptions = Partial<Omit<AnFormModalProps, 'items'>> & {
items: FormItem[]; items: FormItem[];
}; };
export function useFormModalProps(options: FormModalUseOptions) { export function useFormModalProps(options: FormModalUseOptions): AnFormModalProps {
const form = useFormProps({ ...options, submit: undefined }); const { items, model, formProps } = useFormProps({ ...options, submit: undefined });
const props = reactive({ const { trigger, title, submit, modalProps } = options;
...form, return {
title: options.title, model,
trigger: options.trigger, items,
modalProps: options.modalProps, formProps,
submit: options.submit, trigger,
}); title,
return props; modalProps,
submit,
};
} }
export function useFormModal(options: FormModalUseOptions) { export function useFormModal(options: FormModalUseOptions) {
const modalRef = ref<InstanceType<typeof AnFormModal> | null>(null); const modalRef = ref<InstanceType<typeof AnFormModal> | null>(null);
const formRef = computed(() => modalRef.value?.formRef); const formRef = computed(() => modalRef.value?.formRef);
const open = (data: Recordable = {}) => modalRef.value?.open(data); const open = (data: Recordable = {}) => modalRef.value?.open(data);
const props = useFormModalProps(options); const rawProps = useFormModalProps(options);
const props = reactive(rawProps);
const component = () => { const component = () => {
return ( return (

View File

@ -1,12 +1,12 @@
import { defaultsDeep, merge, omit } from 'lodash-es'; import { defaultsDeep, merge, omit } from 'lodash-es';
import { IAnFormItem, IAnFormItemBase } from '../components/FormItem'; import { AnFormItemProps, AnFormItemPropsBase } from '../components/FormItem';
import { SetterItem, setterMap } from '../components/FormSetter'; import { SetterItem, setterMap } from '../components/FormSetter';
import { Rule, useRules } from './useRules'; import { Rule, useRules } from './useRules';
/** /**
* *
*/ */
export type FormItem = Omit<IAnFormItemBase, 'rules'> & export type FormItem = Omit<AnFormItemPropsBase, 'rules'> &
SetterItem & { SetterItem & {
/** /**
* *
@ -41,7 +41,7 @@ const ITEM: Partial<FormItem> = {
}; };
export function useItems(list: FormItem[], model: Recordable) { export function useItems(list: FormItem[], model: Recordable) {
const items = ref<IAnFormItem[]>([]); const items: AnFormItemProps[] = [];
for (const item of list) { for (const item of list) {
let target: any = defaultsDeep({}, ITEM); let target: any = defaultsDeep({}, ITEM);
@ -61,7 +61,7 @@ export function useItems(list: FormItem[], model: Recordable) {
} }
model[item.field] = model[item.field] ?? item.value; model[item.field] = model[item.field] ?? item.value;
items.value.push(target); items.push(target);
} }
return items; return items;

View File

@ -1,6 +1,6 @@
import { FieldRule } from "@arco-design/web-vue"; import { FieldRule } from "@arco-design/web-vue";
import { has, isString } from "lodash-es"; import { has, isString } from "lodash-es";
import { IAnFormItemRule } from "../components/FormItem"; import { AnFormItemRule } from "../components/FormItem";
/** /**
* *
@ -56,7 +56,7 @@ export type FieldStringRule = keyof typeof FieldRuleMap;
/** /**
* *
*/ */
export type Rule = FieldStringRule | IAnFormItemRule; export type Rule = FieldStringRule | AnFormItemRule;
/** /**
* (TS) * (TS)
@ -71,7 +71,7 @@ function defineRuleMap<T extends Record<string, FieldRule>>(ruleMap: T) {
* @returns * @returns
*/ */
export const useRules = <T extends { required?: boolean; rules?: Rule[] }>(item: T) => { export const useRules = <T extends { required?: boolean; rules?: Rule[] }>(item: T) => {
const data: IAnFormItemRule[] = []; const data: AnFormItemRule[] = [];
const { required, rules } = item; const { required, rules } = item;
if (!has(item, "required") && !has(item, "rules")) { if (!has(item, "required") && !has(item, "rules")) {

View File

@ -1,5 +1,16 @@
export * from './components/Form'; export * from './components/Form';
export * from './hooks/useForm';
export * from './components/useFormContext';
export * from './components/FormItem'; export * from './components/FormItem';
export * from './components/FormModal';
export * from './components/FormSetter';
export * from './components/useFormContext';
export * from './components/useFormItems';
export * from './components/useFormModel'; export * from './components/useFormModel';
export * from './components/useFormRef';
export * from './components/useFormSubmit';
export * from './components/useModalSubmit';
export * from './components/useModalTrigger';
export * from './hooks/useForm';
export * from './hooks/useFormModal';
export * from './hooks/useItems';
export * from './hooks/useRules';
export * from './setters';

View File

@ -1,6 +1,5 @@
import { Cascader, CascaderInstance } from '@arco-design/web-vue'; import { Cascader, CascaderInstance } from '@arco-design/web-vue';
import { initOptions } from '../utils/initOptions'; import { defineSetter, initOptions } from './util';
import { defineSetter } from './util';
type CascaderProps = CascaderInstance['$props']; type CascaderProps = CascaderInstance['$props'];

View File

@ -1,6 +1,5 @@
import { Select, SelectInstance } from '@arco-design/web-vue'; import { Select, SelectInstance } from '@arco-design/web-vue';
import { initOptions } from '../utils/initOptions'; import { defineSetter, initOptions } from './util';
import { defineSetter } from './util';
type SelectProps = SelectInstance['$props']; type SelectProps = SelectInstance['$props'];

View File

@ -1,6 +1,5 @@
import { TreeSelect, TreeSelectInstance } from '@arco-design/web-vue'; import { TreeSelect, TreeSelectInstance } from '@arco-design/web-vue';
import { initOptions } from '../utils/initOptions'; import { defineSetter, initOptions } from './util';
import { defineSetter } from './util';
type TreeSelectProps = TreeSelectInstance['$props']; type TreeSelectProps = TreeSelectInstance['$props'];

View File

@ -1,5 +1,5 @@
import { Component } from 'vue'; import { Component } from 'vue';
import { IAnFormItemBase, IAnFormItemSlot } from '../components/FormItem'; import { AnFormItemPropsBase, AnFormItemSlot, AnFormItemFnProps } from '../components/FormItem';
export interface ItemSetter<P extends object, S extends string> { export interface ItemSetter<P extends object, S extends string> {
/** /**
@ -25,15 +25,42 @@ export interface ItemSetter<P extends object, S extends string> {
* } * }
* ``` * ```
*/ */
[key in S]?: IAnFormItemSlot; [key in S]?: AnFormItemSlot;
}; };
/** /**
* *
*/ */
onSetup?: (args: { model: Recordable; item: IAnFormItemBase; items: IAnFormItemBase[] }) => void; onSetup?: (args: { model: Recordable; item: AnFormItemPropsBase; items: AnFormItemPropsBase[] }) => void;
} }
export function defineSetter<P extends object, S extends string>(setter: ItemSetter<P, S>) { export function defineSetter<P extends object, S extends string>(setter: ItemSetter<P, S>) {
return setter; return setter;
} }
export function initOptions({ item, model }: AnFormItemFnProps, key: string = 'options') {
const setterProps: Recordable = item.setterProps!;
if (Array.isArray(item.options) && item.setterProps) {
setterProps[key] = item.options;
return;
}
if (typeof item.options === 'function') {
setterProps[key] = reactive([]);
item.$init = async () => {
const res = await (item as any).options({ item, model });
if (Array.isArray(res)) {
setterProps[key].splice(0);
setterProps[key].push(...res);
return;
}
const data = res?.data?.data;
if (Array.isArray(data)) {
const maped = data.map((i: any) => ({ ...i, value: i.id, label: i.name }));
setterProps[key].splice(0);
setterProps[key].push(...maped);
return;
}
};
item.$init();
}
}

View File

@ -1,26 +0,0 @@
import { Component } from 'vue';
import { IAnFormItemBase, IAnFormItemSlot } from '../components/FormItem';
export interface ItemSetter<P extends object, S extends string> {
/**
*
*/
setter: Component;
/**
*
*/
setterProps?: P;
/**
*
*/
setterSlots?: {
[key in S]?: IAnFormItemSlot;
};
/**
*
*/
onSetup?: (model: Recordable, item: IAnFormItemBase, items: IAnFormItemBase[]) => void;
}

View File

@ -1,28 +0,0 @@
import { IAnFormItemFnProps } from '../components/FormItem';
export function initOptions({ item, model }: IAnFormItemFnProps, key: string = 'options') {
const setterProps: Recordable = item.setterProps!;
if (Array.isArray(item.options) && item.setterProps) {
setterProps[key] = item.options;
return;
}
if (typeof item.options === 'function') {
setterProps[key] = reactive([]);
item.$init = async () => {
const res = await (item as any).options({ item, model });
if (Array.isArray(res)) {
setterProps[key].splice(0);
setterProps[key].push(...res);
return;
}
const data = res?.data?.data;
if (Array.isArray(data)) {
const maped = data.map((i: any) => ({ ...i, value: i.id, label: i.name }));
setterProps[key].splice(0);
setterProps[key].push(...maped);
return;
}
};
item.$init();
}
}

View File

@ -1,9 +0,0 @@
export function strOrFnRender(fn: any, options: any) {
if (typeof fn === "string") {
return () => fn;
}
if (typeof fn === "function") {
return fn(options);
}
return null;
}

View File

@ -1,40 +1,35 @@
import { AnForm, AnFormInstance, IAnForm } from '@/components/AnForm'; import { AnForm, AnFormInstance, AnFormProps } from '@/components/AnForm';
import { AnFormModal } from '@/components/AnForm/components/FormModal'; import { AnFormModal, AnFormModalInstance, AnFormModalProps } from '@/components/AnForm';
import AniEmpty from '@/components/empty/AniEmpty.vue'; import { TableColumnData, TableData, Table, Button, PaginationProps, TableInstance } from '@arco-design/web-vue';
import { FormModalProps } from '@/components/form';
import {
TableColumnData as BaseColumn,
TableData as BaseData,
Table as BaseTable,
Button,
PaginationProps,
} from '@arco-design/web-vue';
import { isArray, isFunction, merge } from 'lodash-es'; import { isArray, isFunction, merge } from 'lodash-es';
import { PropType, defineComponent, ref } from 'vue'; import { InjectionKey, PropType, Ref, defineComponent, ref } from 'vue';
import { PluginContainer } from '../hooks/useTablePlugin'; import { PluginContainer } from '../hooks/useTablePlugin';
import AniEmpty from '@/components/empty/AniEmpty.vue';
type DataFn = (filter: { page: number; size: number; [key: string]: any }) => any | Promise<any>; type DataFn = (filter: { page: number; size: number; [key: string]: any }) => any | Promise<any>;
export const AnTableContextKey = Symbol('AnTableContextKey') as InjectionKey<AnTableContext>;
/** /**
* *
*/ */
export const AnTable = defineComponent({ export const AnTable = defineComponent({
name: 'AnTable', name: 'AnTable',
props: { props: {
/**
*
* @description
*/
data: {
type: [Array, Function] as PropType<BaseData[] | DataFn>,
},
/** /**
* *
*/ */
columns: { columns: {
type: Array as PropType<BaseColumn[]>, type: Array as PropType<TableColumnData[]>,
default: () => [], default: () => [],
}, },
/**
*
* @description
*/
source: {
type: [Array, Function] as PropType<TableData[] | DataFn>,
},
/** /**
* *
*/ */
@ -45,39 +40,42 @@ export const AnTable = defineComponent({
* *
*/ */
search: { search: {
type: Object as PropType<IAnForm>, type: Object as PropType<AnFormProps>,
}, },
/** /**
* *
*/ */
create: { create: {
type: Object as PropType<FormModalProps>, type: Object as PropType<AnFormModalProps>,
}, },
/** /**
* *
*/ */
modify: { modify: {
type: Object as PropType<FormModalProps>, type: Object as PropType<AnFormModalProps>,
}, },
/** /**
* Table * Table
*/ */
tableProps: { tableProps: {
type: Object as PropType<InstanceType<typeof BaseTable>['$props']>, type: Object as PropType<
Omit<TableInstance['$props'], 'ref' | 'pagination' | 'loading' | 'data' | 'onPageChange' | 'onPageSizeChange'>
>,
}, },
/** /**
* *
*/ */
pluginer: { pluginer: {
type: Object as PropType<PluginContainer>, type: Object as PropType<PluginContainer>,
required: true,
}, },
}, },
setup(props) { setup(props) {
const loading = ref(false); const loading = ref(false);
const tableRef = ref<InstanceType<typeof BaseTable>>(); const renderData = ref<TableData[]>([]);
const renderData = ref<BaseData[]>([]); const tableRef = ref<TableInstance | null>(null);
const searchRef = ref<AnFormInstance | null>(null); const searchRef = ref<AnFormInstance | null>(null);
const createRef = ref<AnFormModalInstance | null>(null);
const modifyRef = ref<AnFormModalInstance | null>(null);
const useTablePaging = () => { const useTablePaging = () => {
const getPaging = () => { const getPaging = () => {
@ -114,16 +112,16 @@ export const AnTable = defineComponent({
const paging = getPaging(); const paging = getPaging();
const search = searchRef.value?.getModel() ?? {}; const search = searchRef.value?.getModel() ?? {};
if (isArray(props.data)) { if (isArray(props.source)) {
// todo // todo
} }
if (isFunction(props.data)) { if (isFunction(props.source)) {
try { try {
loading.value = true; loading.value = true;
let params = { ...search, ...paging }; let params = { ...search, ...paging };
params = props.pluginer?.callBeforeSearchHook(params) ?? params; params = props.pluginer?.callBeforeSearchHook(params) ?? params;
const resData = await props.data(params); const resData = await props.source(params);
const { data = [], total = 0 } = resData?.data || {}; const { data = [], total = 0 } = resData?.data || {};
renderData.value = data; renderData.value = data;
setPaging({ total }); setPaging({ total });
@ -145,8 +143,8 @@ export const AnTable = defineComponent({
}; };
watchEffect(() => { watchEffect(() => {
if (Array.isArray(props.data)) { if (Array.isArray(props.source)) {
renderData.value = props.data; renderData.value = props.source;
resetPaging(); resetPaging();
} }
}); });
@ -167,11 +165,13 @@ export const AnTable = defineComponent({
loadData(); loadData();
}; };
const state = { const context: AnTableContext = {
loading, loading,
renderData,
tableRef, tableRef,
searchRef, searchRef,
renderData, createRef,
modifyRef,
loadData, loadData,
reload, reload,
refresh, refresh,
@ -180,17 +180,18 @@ export const AnTable = defineComponent({
props, props,
}; };
props.pluginer?.callSetupHook(state); props.pluginer?.callSetupHook(context);
provide('ref:table', { ...state, ...props }); provide(AnTableContextKey, context);
return state; return context;
}, },
render() { render() {
return ( return (
<div class="table w-full"> <div class="table w-full">
<div class={`mb-3 flex gap-2 toolbar justify-between`}> <div class={`mb-3 flex gap-2 toolbar justify-between`}>
{this.create && <AnFormModal {...(this.create as any)}></AnFormModal>} {this.create && <AnFormModal {...this.create} ref="createRef"></AnFormModal>}
{this.modify && <AnFormModal {...this.modify} trigger={false} ref="modifyRef"></AnFormModal>}
{this.pluginer?.actions && ( {this.pluginer?.actions && (
<div class={`flex-1 flex gap-2 items-center`}> <div class={`flex-1 flex gap-2 items-center`}>
{this.pluginer.actions.map(Action => ( {this.pluginer.actions.map(Action => (
@ -198,8 +199,8 @@ export const AnTable = defineComponent({
))} ))}
</div> </div>
)} )}
<div> {this.search && (
{this.search && ( <div>
<AnForm <AnForm
ref="searchRef" ref="searchRef"
v-model:model={this.search.model} v-model:model={this.search.model}
@ -217,14 +218,18 @@ export const AnTable = defineComponent({
), ),
}} }}
</AnForm> </AnForm>
)} </div>
</div> )}
<div class="flex gap-2"> {this.pluginer?.widgets && (
<div class="flex gap-1">{this.pluginer?.widgets && this.pluginer.widgets?.map(Widget => <Widget />)}</div> <div class="flex gap-2">
</div> {this.pluginer.widgets.map(Widget => (
<Widget />
))}
</div>
)}
</div> </div>
<BaseTable <Table
row-key="id" row-key="id"
bordered={false} bordered={false}
{...this.$attrs} {...this.$attrs}
@ -241,7 +246,7 @@ export const AnTable = defineComponent({
empty: () => <AniEmpty />, empty: () => <AniEmpty />,
...this.$slots, ...this.$slots,
}} }}
</BaseTable> </Table>
</div> </div>
); );
}, },
@ -250,9 +255,57 @@ export const AnTable = defineComponent({
/** /**
* *
*/ */
export type TableInstance = InstanceType<typeof AnTable>; export type AnTableInstance = InstanceType<typeof AnTable>;
/** /**
* *
*/ */
export type TableProps = TableInstance['$props']; export type AnTableProps = Pick<
AnTableInstance['$props'],
'source' | 'columns' | 'search' | 'paging' | 'create' | 'modify' | 'tableProps' | 'pluginer'
>;
export interface AnTableContext {
/**
*
*/
loading: Ref<boolean>;
/**
*
*/
tableRef: Ref<TableInstance | null>;
/**
*
*/
searchRef: Ref<AnFormInstance | null>;
/**
*
*/
createRef: Ref<AnFormModalInstance | null>;
/**
*
*/
modifyRef: Ref<AnFormModalInstance | null>;
/**
*
*/
renderData: Ref<TableData[]>;
/**
*
*/
loadData: () => Promise<void>;
/**
*
*/
reload: () => Promise<void>;
/**
*
*/
refresh: () => Promise<void>;
/**
*
*/
props: AnTableProps;
onPageChange: any;
onPageSizeChange: any;
}

View File

@ -1,178 +0,0 @@
import { Button, Checkbox, Divider, InputNumber, Popover, Scrollbar, Tag } from '@arco-design/web-vue';
import { PropType } from 'vue';
interface Item {
dataIndex: string;
enable: boolean;
autoWidth: boolean;
width: number;
editable: boolean;
title: string;
}
export const TableColumnConfig = defineComponent({
props: {
columns: {
type: Object as PropType<any[]>,
required: true,
},
},
setup(props) {
const checkAll = ref(false);
const visible = ref(false);
const items = ref<Item[]>([]);
const checked = computed(() => items.value.filter(i => i.enable));
const indeterminate = computed(() => {
const check = checked.value.length;
const total = items.value.length;
return 0 < check && check < total;
});
watch(
() => visible.value,
value => {
if (value) {
init();
} else {
}
}
);
const init = () => {
const list: Item[] = [];
for (const column of props.columns) {
list.push({
dataIndex: column.dataIndex,
title: column.title,
enable: true,
autoWidth: !column.width,
width: column.width ?? 60,
editable: !column.configable,
});
}
items.value = list;
};
const onItemChange = () => {
if (checked.value.length === 0) {
checkAll.value = false;
return;
}
if (checked.value.length === items.value.length) {
checkAll.value = true;
}
};
const onCheckAllChange = (value: any) => {
for (const item of items.value) {
if (item.editable) {
item.enable = value;
}
}
};
const onReset = () => {
visible.value = false;
};
const onConfirm = () => {
visible.value = false;
};
const onItemUp = (index: number) => {
[items.value[index - 1], items.value[index]] = [items.value[index], items.value[index - 1]];
};
const onItemDown = (index: number) => {
[items.value[index + 1], items.value[index]] = [items.value[index], items.value[index + 1]];
};
return () => (
<Popover v-model:popup-visible={visible.value} position="br" trigger="click">
{{
default: () => (
<Button class="float-right">{{ icon: () => <span class="icon-park-outline-config"></span> }}</Button>
),
content: () => (
<>
<div class="mb-1 leading-none border-b border-gray-100 pb-3"></div>
<Scrollbar outer-class="h-96 overflow-hidden" class="h-full overflow-auto">
<ul class="grid m-0 p-0 divide-y divide-gray-100 w-[700px] overflow-auto overscroll-contain">
{items.value.map((item, index) => (
<li
key={item.dataIndex}
class="group flex items-center justify-between gap-6 py-2 pr-8 select-none"
>
<div class="flex-1 flex justify-between gap-2">
<Checkbox v-model={item.enable} disabled={!item.editable} onChange={onItemChange}>
{item.title}
</Checkbox>
<span class="hidden group-hover:inline-block ml-4">
<Button
type="text"
shape="circle"
size="mini"
disabled={index == 0}
onClick={() => onItemUp(index)}
>
{{ icon: () => <i class="icon-park-outline-arrow-up"></i> }}
</Button>
<Button
type="text"
shape="circle"
size="mini"
disabled={index == items.value.length - 1}
onClick={() => onItemDown(index)}
>
{{ icon: () => <i class="icon-park-outline-arrow-down"></i> }}
</Button>
</span>
</div>
<div class="flex gap-2 items-center">
<Checkbox v-model={item.autoWidth} disabled={!item.editable}>
{{
checkbox: ({ checked }: any) => (
<Tag checked={checked} checkable={item.editable} color="blue">
</Tag>
),
}}
</Checkbox>
<Divider direction="vertical" margin={8}></Divider>
<InputNumber
size="small"
v-model={item.width}
disabled={item.autoWidth || !item.editable}
min={60}
step={10}
class="!w-20"
/>
<span class="text-gray-400"></span>
</div>
</li>
))}
</ul>
</Scrollbar>
<div class="mt-4 flex gap-2 items-center justify-between">
<div class="flex items-center">
<Checkbox indeterminate={indeterminate.value} v-model={checkAll.value} onChange={onCheckAllChange}>
</Checkbox>
<span class="text-xs text-gray-400 ml-1">
({checked.value.length}/{items.value.length})
</span>
</div>
<div class="space-x-2">
<Button onClick={onReset}></Button>
<Button type="primary" onClick={onConfirm}>
</Button>
</div>
</div>
</>
),
}}
</Popover>
);
},
});

View File

@ -0,0 +1,36 @@
import { dayjs } from '@/libs/dayjs';
import { TableColumn } from '../hooks/useTableColumn';
export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
return {
title: '更新用户',
dataIndex: 'createdAt',
width: 190,
render: ({ record }) => (
<div class="flex flex-col overflow-hidden">
<span>{record.updatedBy ?? '无'}</span>
<span class="text-gray-400 text-xs truncate">
{dayjs(record.updatedAt).format()}
</span>
</div>
),
...extra,
};
}
export function useCreateColumn(extra: TableColumn = {}): TableColumn {
return {
title: '创建用户',
dataIndex: 'createdAt',
width: 190,
render: ({ record }) => (
<div class="flex flex-col overflow-hidden">
<span>{record.createdBy ?? '无'}</span>
<span class="text-gray-400 text-xs truncate">
{dayjs(record.createdAt).format()}
</span>
</div>
),
...extra,
};
}

View File

@ -1,34 +0,0 @@
import { dayjs } from '@/libs/dayjs';
import { TableColumn } from '../hooks/useTableColumn';
export function useUpdateColumn(): TableColumn {
return {
title: '更新用户',
dataIndex: 'createdAt',
width: 190,
render({ record }) {
return (
<div class="flex flex-col overflow-hidden">
<span>{record.updatedBy ?? '无'}</span>
<span class="text-gray-400 text-xs truncate">{dayjs(record.updatedAt).format()}</span>
</div>
);
},
};
}
export function useCreateColumn(): TableColumn {
return {
title: '创建用户',
dataIndex: 'createdAt',
width: 190,
render({ record }) {
return (
<div class="flex flex-col overflow-hidden">
<span>{record.createdBy ?? '无'}</span>
<span class="text-gray-400 text-xs truncate">{dayjs(record.createdAt).format()}</span>
</div>
);
},
};
}

View File

@ -0,0 +1,40 @@
import { FormModalUseOptions, useFormModalProps } from '@/components/AnForm';
export type UseCreateFormOptions = FormModalUseOptions & {
/**
*
* @description `modalProps.width` 便
* @example
* ```ts
* 580
* ```
*/
width?: number;
/**
*
* @description `formProps.class` 便
* @example
* ```ts
* 'grid grid-cols-2'
* ```
*/
formClass?: unknown;
};
export function useCreateForm(options: UseCreateFormOptions) {
if (options.width) {
if (!options.modalProps) {
(options as any).modalProps = {};
}
(options.modalProps as any).width = options.width;
delete options.width;
}
if (options.formClass) {
if (!options.formProps) {
(options as any).formProps = {};
}
options.formProps!.class = options.formClass;
delete options.formClass;
}
return useFormModalProps(options);
}

View File

@ -1,42 +1,89 @@
import { FormItem } from '@/components/AnForm/hooks/useItems'; import { FormItem, FormModalUseOptions, useFormModalProps, AnFormModalProps } from '@/components/AnForm';
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
import { FormModalUseOptions } from '../../AnForm/hooks/useFormModal';
import { ExtendFormItem } from './useSearchForm'; import { ExtendFormItem } from './useSearchForm';
import { TableUseOptions } from './useTable'; import { TableUseOptions } from './useTable';
export type ModifyForm = Omit<FormModalUseOptions, 'items'> & { export type ModifyForm = Omit<FormModalUseOptions, 'items' | 'trigger'> & {
/**
*
* @description `modalProps.width` 便
* @example
* ```ts
* 580
* ```
*/
width?: number;
/**
*
* @description `formProps.class` 便
* @example
* ```ts
* 'grid grid-cols-2'
* ```
*/
formClass?: unknown;
/** /**
* *
* @default
* ```ts
* false
* ```
*/ */
extend?: boolean; extend?: boolean;
/** /**
* *
* ```tsx
* [{
* extend: 'name', // 从 create.items 中继承
* }]
* ```
*/ */
items?: ExtendFormItem[]; items?: ExtendFormItem[];
}; };
export function useModifyForm(options: TableUseOptions) { export function useModifyForm(options: TableUseOptions): AnFormModalProps | undefined {
const { create, modify } = options; const { create, modify } = options;
if (!modify) { if (!modify) {
return null; return undefined;
} }
const { extend, items, ...rest } = modify;
let result = {}; let result: FormModalUseOptions = { items: [] };
if (extend && create) { if (modify.extend && create) {
const { items: createItems, ...createRest } = create; result = merge({}, create);
const createItemMap = createItems.reduce((map, value) => { }
map[value.field] = value; result = merge(result, modify);
return map;
}, {} as Record<string, FormItem>); if (modify.items) {
const modified: any = merge({}, createRest, rest); const items: FormItem[] = [];
const modifyItems = items; const createItemMap: Record<string, FormItem> = {};
if (modifyItems) { for (const item of create?.items ?? []) {
for (let i = 0; i < modifyItems.length; i++) { createItemMap[item.field] = item;
if (modifyItems[i].extend) { }
modifyItems[i] = merge({}, createItemMap[modifyItems[i].field!], modifyItems[i]); for (let item of modify.items ?? []) {
} if (item.extend) {
} item = merge({}, createItemMap[item.field!] ?? {}, item);
}
items.push(item as any);
}
if (items.length) {
result.items = items;
} }
modified.items = modifyItems;
} }
if (modify.width || create?.width) {
if (!result.modalProps) {
(result as any).modalProps = {};
}
(result.modalProps as any).width = modify.width || create?.width;
}
if (modify.formClass || create?.formClass) {
if (!result.formProps) {
(result as any).formProps = {};
}
result.formProps!.class = modify.formClass || create?.formClass;
}
return useFormModalProps(result);
} }

View File

@ -1,13 +1,14 @@
import { defaultsDeep, isArray, merge } from 'lodash-es'; import { defaultsDeep, isArray, merge } from 'lodash-es';
import { FormUseOptions } from '../../AnForm'; import { AnFormProps, FormUseOptions, AnFormItemProps, FormItem, useItems } from '@/components/AnForm';
import { IAnFormItem } from '../../AnForm/components/FormItem';
import { FormItem, useItems } from '../../AnForm/hooks/useItems';
export type ExtendFormItem = Partial< export type ExtendFormItem = Partial<
FormItem & { FormItem & {
/** /**
* *
* @example 'name' * @example
* ```ts
* 'name'
* ```
*/ */
extend: string; extend: string;
} }
@ -16,12 +17,18 @@ export type ExtendFormItem = Partial<
type SearchFormItem = ExtendFormItem & { type SearchFormItem = ExtendFormItem & {
/** /**
* *
* @default false * @default
* ```ts
* false
* ```
*/ */
searchable?: boolean; searchable?: boolean;
/** /**
* *
* @default false * @default
* ```ts
* false
* ```
*/ */
enterable?: boolean; enterable?: boolean;
}; };
@ -29,20 +36,29 @@ type SearchFormItem = ExtendFormItem & {
export type SearchFormObject = Omit<FormUseOptions, 'items' | 'submit'> & { export type SearchFormObject = Omit<FormUseOptions, 'items' | 'submit'> & {
/** /**
* *
* @example
* ```ts
* [{
* extend: 'name' // 从 create.items 继承
* }]
* ```
*/ */
items?: SearchFormItem[]; items?: SearchFormItem[];
/** /**
* *
* @default false * @default
* ```tsx
* false
* ```
*/ */
hideSearch?: boolean; hideSearch?: boolean;
}; };
export type SearchForm = SearchFormObject | SearchFormItem[]; export type SearchForm = SearchFormObject | SearchFormItem[];
export function useSearchForm(search?: SearchForm, extendItems: IAnFormItem[] = []) { export function useSearchForm(search?: SearchForm, extendItems: AnFormItemProps[] = []): AnFormProps | undefined {
if (!search) { if (!search) {
return ref(); return undefined;
} }
if (isArray(search)) { if (isArray(search)) {
@ -50,15 +66,15 @@ export function useSearchForm(search?: SearchForm, extendItems: IAnFormItem[] =
} }
const { items: _items = [], hideSearch, model: _model, formProps: _formProps } = search; const { items: _items = [], hideSearch, model: _model, formProps: _formProps } = search;
const extendMap = extendItems.reduce((m, v) => ((m[v.field] = v), m), {} as Record<string, IAnFormItem>); const extendMap = extendItems.reduce((m, v) => ((m[v.field] = v), m), {} as Record<string, AnFormItemProps>);
const props = ref({ const props = {
items: [] as IAnFormItem[], items: [] as AnFormItemProps[],
model: _model ?? {}, model: _model ?? {},
formProps: defaultsDeep({}, _formProps, { layout: 'inline' }), formProps: defaultsDeep({}, _formProps, { layout: 'inline' }),
}); };
const defualts: Partial<IAnFormItem> = { const defualts: Partial<AnFormItemProps> = {
setter: 'input', setter: 'input',
itemProps: { itemProps: {
hideLabel: true, hideLabel: true,
@ -66,13 +82,13 @@ export function useSearchForm(search?: SearchForm, extendItems: IAnFormItem[] =
setterProps: {}, setterProps: {},
}; };
const items: IAnFormItem[] = []; const items: AnFormItemProps[] = [];
for (const _item of _items) { for (const _item of _items) {
const { searchable, enterable, field, extend, ...itemRest } = _item; const { searchable, enterable, field, extend, ...itemRest } = _item;
if ((field || extend) === 'submit' && hideSearch) { if ((field || extend) === 'submit' && hideSearch) {
continue; continue;
} }
let item: IAnFormItem = defaultsDeep({}, itemRest, defualts); let item: AnFormItemProps = defaultsDeep({}, itemRest, defualts);
if (extend) { if (extend) {
const extendItem = extendMap[extend]; const extendItem = extendMap[extend];
if (extendItem) { if (extendItem) {
@ -91,7 +107,7 @@ export function useSearchForm(search?: SearchForm, extendItems: IAnFormItem[] =
items.push(item); items.push(item);
} }
props.value.items = useItems(items, props.value.model).value; props.items = useItems(items, props.model);
return props; return props;
} }

View File

@ -1,11 +1,12 @@
import { FormModalUseOptions, useFormModalProps } from '../../AnForm/hooks/useFormModal'; import { useFormModalProps } from '@/components/AnForm';
import { AnTable, TableProps } from '../components/Table'; import { AnTable, AnTableInstance, AnTableProps } from '../components/Table';
import { ModifyForm, useModifyForm } from './useModiyForm'; import { ModifyForm, useModifyForm } from './useModiyForm';
import { SearchForm, useSearchForm } from './useSearchForm'; import { SearchForm, useSearchForm } from './useSearchForm';
import { TableColumn, useTableColumns } from './useTableColumn'; import { TableColumn, useTableColumns } from './useTableColumn';
import { AnTablePlugin, PluginContainer } from './useTablePlugin'; import { AnTablePlugin, PluginContainer } from './useTablePlugin';
import { UseCreateFormOptions } from './useCreateForm';
export interface TableUseOptions extends Pick<TableProps, 'data' | 'tableProps' | 'paging'> { export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tableProps' | 'paging'> {
/** /**
* ID * ID
* @example * @example
@ -27,8 +28,8 @@ export interface TableUseOptions extends Pick<TableProps, 'data' | 'tableProps'
* @example * @example
* ```ts * ```ts
* [{ * [{
* dataIndex: 'title', * dataIndex: 'title',
* title: '标题' * title: '标题'
* }] * }]
* ``` * ```
*/ */
@ -38,9 +39,9 @@ export interface TableUseOptions extends Pick<TableProps, 'data' | 'tableProps'
* @example * @example
* ```ts * ```ts
* [{ * [{
* field: 'name', * field: 'name',
* label: '用户名称', * label: '用户名称',
* setter: 'input' * setter: 'input'
* }] * }]
* ``` * ```
*/ */
@ -56,7 +57,7 @@ export interface TableUseOptions extends Pick<TableProps, 'data' | 'tableProps'
* } * }
* ``` * ```
*/ */
create?: FormModalUseOptions; create?: UseCreateFormOptions;
/** /**
* *
* @example * @example
@ -71,52 +72,54 @@ export interface TableUseOptions extends Pick<TableProps, 'data' | 'tableProps'
modify?: ModifyForm; modify?: ModifyForm;
} }
export function useTableProps(options: TableUseOptions) { export function useTableProps(options: TableUseOptions): AnTableProps {
const { columns } = useTableColumns(options.columns ?? []); const { source, tableProps } = options;
const paging = ref({ hide: false, showTotal: true, showPageSize: true, ...(options.paging ?? {}) });
const columns = useTableColumns(options.columns ?? []);
const paging = { hide: false, showTotal: true, showPageSize: true, ...(options.paging ?? {}) };
const search = options.search && useSearchForm(options.search); const search = options.search && useSearchForm(options.search);
const create = options.create && useFormModalProps(options.create); const create = options.create && useFormModalProps(options.create);
const modify = options.modify && useModifyForm(options); const modify = options.modify && useModifyForm(options);
const props = reactive({
data: options.data, return {
tableProps: options.tableProps, tableProps,
columns, columns,
source,
search, search,
paging, paging,
create, create,
modify, modify,
}); };
props;
} }
export function useTable(options: TableUseOptions) { export function useTable(options: TableUseOptions) {
const tableRef = ref<InstanceType<typeof AnTable> | null>(null); const tableRef = ref<AnTableInstance | null>(null);
const pluginer = new PluginContainer(options.plugins ?? []); if (!options.plugins) {
options.plugins = [];
}
const pluginer = new PluginContainer(options.plugins);
options = pluginer.callOptionsHook(options); options = pluginer.callOptionsHook(options);
const { columns } = useTableColumns(options.columns ?? []); const rawProps = useTableProps(options);
const data = ref(options.data); const props = reactive(rawProps);
const pagination = ref({ hide: false, showTotal: true, showPageSize: true, ...(options.paging ?? {}) });
const tableProps = ref(options.tableProps ?? {});
const searchProps = useSearchForm(options.search);
const create = options.create && useFormModalProps(options.create);
const AnTabler = () => ( const AnTabler = () => (
<AnTable <AnTable
ref={(el: any) => (tableRef.value = el)} ref={(el: any) => (tableRef.value = el)}
columns={columns.value} tableProps={props.tableProps}
data={data.value} columns={props.columns}
paging={pagination.value} source={props.source}
tableProps={tableProps.value} paging={props.paging}
search={searchProps.value} search={props.search}
create={props.create as any}
modify={props.modify as any}
pluginer={pluginer} pluginer={pluginer}
create={create as any}
></AnTable> ></AnTable>
); );
return { return {
component: AnTabler, component: AnTabler,
columns,
tableRef, tableRef,
props,
}; };
} }

View File

@ -23,7 +23,13 @@ interface TableColumnButton {
* ``` * ```
*/ */
type?: 'modify' | 'delete'; type?: 'modify' | 'delete';
/**
*
* @example
* ```ts
* '确定删除吗?'
* ```
*/
confirm?: string; confirm?: string;
/** /**
* *
@ -61,7 +67,7 @@ interface TableColumnButton {
* (props) => api.user.rmUser(props.record.id) * (props) => api.user.rmUser(props.record.id)
* ``` * ```
*/ */
onClick?: (props: any) => void; onClick?: (props: any) => any | Promise<any>;
} }
interface TableButtonColumn { interface TableButtonColumn {
@ -73,7 +79,11 @@ interface TableButtonColumn {
* *
* @example * @example
* ```ts * ```ts
* [{ text: '删除', onClick: (args) => api.user.rmUser(args.record.id) }] * [{
* type: 'delete',
* text: '删除',
* onClick: (args) => api.user.rmUser(args.record.id)
* }]
* ``` * ```
*/ */
buttons: TableColumnButton[]; buttons: TableColumnButton[];
@ -103,30 +113,25 @@ export type TableColumn = TableColumnData &
}; };
export function useTableColumns(data: TableColumn[]) { export function useTableColumns(data: TableColumn[]) {
const columns = ref<TableColumnData[]>([]); const columns: TableColumnData[] = [];
// for (let column of data) {
// if (column.type === "index") {
// column = useTableIndexColumn(column);
// }
for (let column of data) { for (let column of data) {
// if (column.type === "index") {
// column = useTableIndexColumn(column);
// }
if (column.type === 'button') { if (column.type === 'button') {
column = useTableButtonColumn(column); column = useTableButtonColumn(column);
} }
columns.value.push(column);
// if (column.type === "dropdown") {
// column = useTableDropdownColumn(column);
// }
columns.push(column);
} }
// if (column.type === "dropdown") { return columns;
// column = useTableDropdownColumn(column);
// }
// columns.push({ ...config.columnBase, ...column });
// }
return {
columns,
};
} }
function useTableIndexColumn() {} function useTableIndexColumn() {}

View File

@ -1,179 +0,0 @@
import { Button, Checkbox, Divider, InputNumber, Popover, Scrollbar, Tag } from '@arco-design/web-vue';
import { PropType } from 'vue';
interface Item {
dataIndex: string;
enable: boolean;
autoWidth: boolean;
width: number;
editable: boolean;
title: string;
}
export const TableColumnConfig = defineComponent({
props: {
columns: {
type: Object as PropType<any[]>,
required: true,
},
},
setup(props) {
const checkAll = ref(false);
const visible = ref(false);
const items = ref<Item[]>([]);
const checked = computed(() => items.value.filter(i => i.enable));
const indeterminate = computed(() => {
const check = checked.value.length;
const total = items.value.length;
return 0 < check && check < total;
});
watch(
() => visible.value,
value => {
if (value) {
init();
} else {
}
}
);
const init = () => {
const list: Item[] = [];
for (const column of props.columns) {
list.push({
dataIndex: column.dataIndex,
title: column.title,
enable: true,
autoWidth: !column.width,
width: column.width ?? 60,
editable: !column.configable,
});
}
items.value = list;
};
const onItemChange = () => {
if (checked.value.length === 0) {
checkAll.value = false;
return;
}
if (checked.value.length === items.value.length) {
checkAll.value = true;
}
};
const onCheckAllChange = (value: any) => {
for (const item of items.value) {
if (item.editable) {
item.enable = value;
}
}
};
const onReset = () => {
visible.value = false;
};
const onConfirm = () => {
visible.value = false;
};
const onItemUp = (index: number) => {
[items.value[index - 1], items.value[index]] = [items.value[index], items.value[index - 1]];
};
const onItemDown = (index: number) => {
[items.value[index + 1], items.value[index]] = [items.value[index], items.value[index + 1]];
};
return () => (
<Popover v-model:popup-visible={visible.value} position="br" trigger="click">
{{
default: () => (
<Button class="float-right">{{ icon: () => <span class="icon-park-outline-config"></span> }}</Button>
),
content: () => (
<>
<div class="mb-1 leading-none border-b border-gray-100 pb-3"></div>
<Scrollbar outer-class="h-96 overflow-hidden" class="h-full overflow-auto">
<ul class="grid m-0 p-0 divide-y divide-gray-100 w-[700px] overflow-auto overscroll-contain">
{items.value.map((item, index) => (
<li
key={item.dataIndex}
class="group flex items-center justify-between gap-6 py-2 pr-8 select-none"
>
<div class="flex-1 flex justify-between gap-2">
<Checkbox v-model={item.enable} disabled={!item.editable} onChange={onItemChange}>
{item.title}
</Checkbox>
<span class="hidden group-hover:inline-block ml-4">
<Button
type="text"
shape="circle"
size="mini"
disabled={index == 0}
onClick={() => onItemUp(index)}
>
{{ icon: () => <i class="icon-park-outline-arrow-up"></i> }}
</Button>
<Button
type="text"
shape="circle"
size="mini"
disabled={index == items.value.length - 1}
onClick={() => onItemDown(index)}
>
{{ icon: () => <i class="icon-park-outline-arrow-down"></i> }}
</Button>
</span>
</div>
<div class="flex gap-2 items-center">
<Checkbox v-model={item.autoWidth} disabled={!item.editable}>
{{
checkbox: ({ checked }: any) => (
<Tag checked={checked} checkable={item.editable} color="blue">
</Tag>
),
}}
</Checkbox>
<Divider direction="vertical" margin={8}></Divider>
<InputNumber
size="small"
v-model={item.width}
disabled={item.autoWidth || !item.editable}
min={60}
step={10}
class="!w-20"
/>
<span class="text-gray-400"></span>
</div>
</li>
))}
</ul>
</Scrollbar>
<div class="mt-4 flex gap-2 items-center justify-between">
<div class="flex items-center">
<Checkbox indeterminate={indeterminate.value} v-model={checkAll.value} onChange={onCheckAllChange}>
</Checkbox>
<span class="text-xs text-gray-400 ml-1">
({checked.value.length}/{items.value.length})
</span>
</div>
<div class="space-x-2">
<Button onClick={onReset}></Button>
<Button type="primary" onClick={onConfirm}>
</Button>
</div>
</div>
</>
),
}}
</Popover>
);
},
});

View File

@ -1,14 +1,23 @@
import { Component } from 'vue';
import { AnTableContext } from '../components/Table';
import { TableUseOptions } from './useTable'; import { TableUseOptions } from './useTable';
import { TableColumn } from './useTableColumn';
import { useTableRefresh } from '../plugins/useTableRefresh';
import { useColumnConfig } from '../plugins/useTableConfig';
import { useRowFormat } from '../plugins/useRowFormat';
import { useRowDelete } from '../plugins/useRowDelete';
import { useRowModify } from '../plugins/useRowModify';
export interface AnTablePlugin { export interface AnTablePlugin {
/** /**
* ID() * ID()
* @example * @example
* ```ts * ```ts
* 'Plugin:Refresh' * 'refresh'
* ``` * ```
*/ */
id: string; id: string;
/** /**
* 使 * 使
* @example * @example
@ -17,22 +26,42 @@ export interface AnTablePlugin {
* ``` * ```
*/ */
provide?: Recordable; provide?: Recordable;
/** /**
* *
* @description `setup`
*/ */
onSetup?: (context: any) => void; onSetup?: (context: AnTableContext) => void;
/** /**
* *
* @description
*/ */
options?: (options: TableUseOptions) => TableUseOptions | null | undefined | void; options?: (options: TableUseOptions) => TableUseOptions | null | undefined | void;
/**
*
*/
column?: (column: TableColumn) => TableColumn;
/** /**
* *
* @example
* ```tsx
* () => <Button></Button>
* ```
*/ */
widget?: () => any; widget?: () => (props: any) => any | Component;
/** /**
* *
* @example
* ```tsx
* () => <Button></Button>
* ```
*/ */
action?: () => any; action?: () => (props: any) => any | Component;
/** /**
* *
* *
@ -47,19 +76,20 @@ export class PluginContainer {
widgets: any[] = []; widgets: any[] = [];
constructor(private plugins: AnTablePlugin[]) { constructor(private plugins: AnTablePlugin[]) {
this.plugins.unshift(useTableRefresh(), useColumnConfig(), useRowFormat(), useRowDelete(), useRowModify());
for (const plugin of plugins) { for (const plugin of plugins) {
const action = plugin.action?.(); const action = plugin.action?.();
const widget = plugin.widget?.();
if (action) { if (action) {
this.actions.push(action); this.actions.push(action);
} }
const widget = plugin.widget?.();
if (widget) { if (widget) {
this.widgets.push(widget); this.widgets.push(widget);
} }
} }
} }
callSetupHook(context: any) { callSetupHook(context: AnTableContext) {
for (const plugin of this.plugins) { for (const plugin of this.plugins) {
plugin.onSetup?.(context); plugin.onSetup?.(context);
} }

View File

@ -1 +1,14 @@
export * from './components/column';
export * from './components/Table';
export * from './hooks/useTable'; export * from './hooks/useTable';
export * from './hooks/useTablePlugin';
export * from './hooks/useTableColumn';
export * from './hooks/useSearchForm';
export * from './hooks/useModiyForm';
export * from './plugins/useTableConfig';
export * from './plugins/useTableRefresh';
export * from './plugins/useTableSelect';
export * from './plugins/useTableDelete';
export * from './plugins/useRowDelete';
export * from './plugins/useRowModify';
export * from './plugins/useRowFormat';

View File

@ -0,0 +1,42 @@
import { delConfirm, delOptions } from '@/utils';
import { AnTableContext } from '../components/Table';
import { AnTablePlugin } from '../hooks/useTablePlugin';
import { Message } from '@arco-design/web-vue';
export function useRowDelete(): AnTablePlugin {
let ctx: AnTableContext;
return {
id: 'rowDelete',
onSetup(context) {
ctx = context;
},
options(options) {
for (const column of options.columns ?? []) {
if (column.type !== 'button') {
continue;
}
const btn = column.buttons.find(i => i.type === 'delete');
if (!btn) {
continue;
}
const onClick = btn.onClick;
btn.onClick = async props => {
let confirm = btn.confirm ?? {};
if (typeof confirm === 'string') {
confirm = { content: confirm };
}
delConfirm({
...delOptions,
...confirm,
async onBeforeOk() {
const res: any = await onClick?.(props);
const msg = res?.data?.message;
msg && Message.success(`提示: ${msg}`);
ctx.refresh();
},
});
};
}
},
};
}

View File

@ -0,0 +1,15 @@
import { AnTablePlugin } from '../hooks/useTablePlugin';
export function useRowFormat(): AnTablePlugin {
return {
id: 'format',
options(options) {
for (const column of options.columns ?? []) {
if (column.render) {
continue;
}
column.render = ({ record, column }) => record[column.dataIndex!] ?? '-';
}
},
};
}

View File

@ -0,0 +1,28 @@
import { AnTableContext } from '../components/Table';
import { AnTablePlugin } from '../hooks/useTablePlugin';
export function useRowModify(): AnTablePlugin {
let ctx: AnTableContext;
return {
id: 'rowModify',
onSetup(context) {
ctx = context;
},
options(options) {
for (const column of options.columns ?? []) {
if (column.type !== 'button') {
continue;
}
const btn = column.buttons.find(i => i.type === 'modify');
if (!btn) {
continue;
}
const onClick = btn.onClick;
btn.onClick = async props => {
const { modifyRef } = ctx ?? {};
modifyRef?.value?.open(props.record);
};
}
},
};
}

View File

@ -1,6 +1,7 @@
import { Button, Checkbox, Divider, InputNumber, Popover, Scrollbar, Tag } from '@arco-design/web-vue'; import { Button, Checkbox, Divider, InputNumber, Popover, Scrollbar, Tag } from '@arco-design/web-vue';
import { PropType } from 'vue'; import { PropType } from 'vue';
import { AnTablePlugin } from '../hooks/useTablePlugin'; import { AnTablePlugin } from '../hooks/useTablePlugin';
import { AnTableContext } from '../components/Table';
interface Item { interface Item {
dataIndex: string; dataIndex: string;
@ -183,14 +184,14 @@ export const TableColumnConfig = defineComponent({
* @description ID * @description ID
*/ */
export function useColumnConfig(): AnTablePlugin { export function useColumnConfig(): AnTablePlugin {
let context: any; let context: AnTableContext;
return { return {
id: "columnconfig", id: 'columnconfig',
onSetup(args) { onSetup(ctx) {
context = args; context = ctx;
}, },
widget() { widget() {
return () => <TableColumnConfig columns={context.props.columns} />; return () => <TableColumnConfig columns={context.props.columns!} />;
}, },
}; };
} }

View File

@ -0,0 +1,48 @@
import { Ref } from 'vue';
import { AnTableContext } from '../components/Table';
import { AnTablePlugin } from '../hooks/useTablePlugin';
import { useTableSelect } from './useTableSelect';
import { delConfirm, delOptions, sleep } from '@/utils';
import { Button, Message } from '@arco-design/web-vue';
export function useTableDelete(): AnTablePlugin {
let selected: Ref<any[]>;
let context: AnTableContext;
return {
id: 'deletemany',
options(options) {
let selectPlugin = options.plugins?.find(i => i.id === 'selection');
if (!selectPlugin) {
selectPlugin = useTableSelect();
options.plugins!.push(selectPlugin);
}
selected = selectPlugin.provide!.selectedKeys;
return options;
},
action() {
const onClick = async (props: any) => {
delConfirm({
...delOptions,
content: '危险操作,确定删除所选数据吗?',
async onBeforeOk() {
await sleep(3000);
const res: any = await onClick?.(props);
const msg = res?.data?.message;
msg && Message.success(`提示: ${msg}`);
selected.value = [];
context.refresh();
},
});
};
return props => (
<Button status="danger" disabled={!selected.value.length} onClick={() => onClick(props)}>
{{
icon: () => <i class="icon-park-outline-delete" />,
default: () => '删除',
}}
</Button>
);
},
};
}

View File

@ -1,12 +1,13 @@
import { Button } from '@arco-design/web-vue'; import { Button } from '@arco-design/web-vue';
import { AnTablePlugin } from '../hooks/useTablePlugin'; import { AnTablePlugin } from '../hooks/useTablePlugin';
import { AnTableContext } from '../components/Table';
/** /**
* *
* @description * @description
*/ */
export function useRefresh(): AnTablePlugin { export function useTableRefresh(): AnTablePlugin {
let context: any = {}; let context: AnTableContext;
return { return {
id: 'refresh', id: 'refresh',

View File

@ -16,13 +16,24 @@ const defaults: TableUseOptions = {
}, },
}; };
export function useSelection<T extends any>({ key = 'id', mode = 'key' } = {}): AnTablePlugin { interface UseTableSelectOptions {
const selected = ref<T[]>([]); key?: string;
mode?: 'key' | 'row' | 'both';
}
/**
*
* @description 使
*/
export function useTableSelect({ key = 'id', mode = 'key' }: UseTableSelectOptions = {}): AnTablePlugin {
const selectedKeys = ref<(string | number)[]>([]);
const selectedRows = ref<any[]>([]);
return { return {
id: 'selection', id: 'selection',
provide: { provide: {
selected, selectedKeys,
selectedRows,
}, },
options(options) { options(options) {
const opts: TableUseOptions = defaultsDeep({}, defaults); const opts: TableUseOptions = defaultsDeep({}, defaults);
@ -31,24 +42,24 @@ export function useSelection<T extends any>({ key = 'id', mode = 'key' } = {}):
opts.tableProps!.rowKey = key; opts.tableProps!.rowKey = key;
} }
if (mode === 'key') { if (mode === 'both' || mode === 'key') {
opts.tableProps!.onSelectionChange = rowkeys => { opts.tableProps!.onSelectionChange = rowkeys => {
selected.value = rowkeys as any[]; selectedKeys.value = rowkeys;
}; };
} }
if (mode === 'row') { if (mode === 'both' || mode === 'row') {
opts.tableProps!.onSelect = (rowkeys, rowkey, record) => { opts.tableProps!.onSelect = (rowkeys, rowkey, record) => {
const index = selected.value.findIndex((i: any) => i[key] == record[key]); const index = selectedRows.value.findIndex((i: any) => i[key] == record[key]);
if (index > -1) { if (index > -1) {
selected.value.splice(index, 1); selectedRows.value.splice(index, 1);
} }
}; };
opts.tableProps!.onSelectAll = checked => { opts.tableProps!.onSelectAll = checked => {
if (checked) { if (checked) {
selected.value = cloneDeep([]); selectedRows.value = cloneDeep([]);
} else { } else {
selected.value = []; selectedRows.value = [];
} }
}; };
} }

View File

@ -1,76 +0,0 @@
import { Form as BaseForm, FormInstance as BaseFormInstance } from "@arco-design/web-vue";
import { PropType } from "vue";
import { FormContextKey } from "../core/interface";
import { useFormItems } from "../core/useFormItems";
import { useFormModel } from "../core/useFormModel";
import { useFormRef } from "../core/useFormRef";
import { useFormSubmit } from "../core/useFormSubmit";
import { AnFormItem, IAnFormItem } from "./FormItem";
import { SubmitFn } from "./types/Form";
import { useVModel } from "@vueuse/core";
/**
*
*/
export const AnForm = defineComponent({
name: "AnForm",
props: {
/**
*
*/
model: {
type: Object as PropType<Recordable>,
required: true,
},
/**
*
*/
items: {
type: Array as PropType<IAnFormItem[]>,
default: () => [],
},
/**
*
*/
submit: {
type: Function as PropType<IAnFormSubmit>,
},
/**
* Form
*/
formProps: {
type: Object as PropType<Omit<BaseFormInstance["$props"], "model">>,
},
},
emits: ["update:model"],
setup(props, { slots, emit }) {
const model = useVModel(props, "model", emit);
const items = computed(() => props.items);
const submit = computed(() => props.submit);
const formRefes = useFormRef();
const formModel = useFormModel(model, formRefes.clearValidate);
const formItems = useFormItems(items, model);
const formSubmit = useFormSubmit({ items, model, validate: formRefes.validate, submit }, formModel.getModel);
const context = { slots, ...formModel, ...formItems, ...formRefes, ...formSubmit };
provide(FormContextKey, context);
return context;
},
render() {
return (
<BaseForm layout="vertical" {...this.$attrs} {...this.formProps} ref="formRef" model={this.model}>
{this.items.map((item) => (
<AnFormItem key={item.field} item={item} items={this.items} model={this.model}></AnFormItem>
))}
</BaseForm>
);
},
});
export type AnFormInstance = InstanceType<typeof AnForm>;
export type AnFormProps = AnFormInstance["$props"];
export type IAnForm = Pick<AnFormProps, "model" | "items" | "submit" | "formProps">;
export type IAnFormSubmit = (model: Recordable, items: IAnFormItem) => any;

View File

@ -1,131 +0,0 @@
import { FormItem as BaseFormItem, FieldRule, FormItemInstance } from "@arco-design/web-vue";
import { isFunction } from "lodash-es";
import { PropType } from "vue";
import { NodeType, NodeUnion, nodeMap } from "../nodes";
/**
*
*/
export const AnFormItem = defineComponent({
name: "AnFormItem",
props: {
/**
*
*/
item: {
type: Object as PropType<IAnFormItem>,
required: true,
},
/**
*
*/
items: {
type: Array as PropType<IAnFormItem[]>,
required: true,
},
/**
*
*/
model: {
type: Object as PropType<Recordable>,
required: true,
},
},
setup(props) {
const rules = computed(() => props.item.rules?.filter((i) => !i.disable?.(props.model, props.item, props.items)));
const disabled = computed(() => Boolean(props.item.disable?.(props.model, props.item, props.items)));
const label = strOrFnRender(props.item.label, props);
const help = strOrFnRender(props.item.help, props);
const extra = strOrFnRender(props.item.extra, props);
const render = () => {
let render = (props.item as any).render;
if (!render) {
return null;
}
if (typeof render === "string") {
render = nodeMap[render as NodeType]?.render;
if (!render) {
return null;
}
return <render {...props.item.nodeProps} v-model={props.model[props.item.field]} />;
}
if (isFunction(render)) {
return <render {...props.item.nodeProps} items={props.items} model={props.model} item={props.item} />;
}
};
return () => {
if (props.item.visible && !props.item.visible(props.model, props.item, props.items)) {
return null;
}
return (
<BaseFormItem {...props.item.itemProps} rules={rules.value} disabled={disabled.value} field={props.item.field}>
{{ default: render, label, help, extra }}
</BaseFormItem>
);
};
},
});
export function strOrFnRender(fn: any, ...args: any[]) {
if (typeof fn === "string") {
return () => fn;
}
if (typeof fn === "function") {
return fn(...args);
}
return null;
}
export type IAnFormItemBoolFn = (model: Recordable, item: IAnFormItem, items: IAnFormItem[]) => boolean;
export type IAnFormItemElemFn = (model: Recordable, item: IAnFormItem, items: IAnFormItem[]) => any;
export type IAnFormItemRule = FieldRule & { disable?: IAnFormItemBoolFn };
export type IAnFormItemBase = {
/**
*
*/
field: string;
/**
* `FormItem`
*/
itemProps?: Partial<Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">>;
/**
*
*/
rules?: IAnFormItemRule[];
/**
*
*/
visible?: IAnFormItemBoolFn;
/**
*
*/
disable?: IAnFormItemBoolFn;
/**
*
*/
label?: string | IAnFormItemElemFn;
/**
*
*/
help?: string | IAnFormItemElemFn;
/**
*
*/
extra?: string | IAnFormItemElemFn;
init?: any;
};
export type IAnFormItem = IAnFormItemBase & NodeUnion;

View File

@ -1,16 +0,0 @@
import { nodeMap, NodeMap, NodeType } from "../nodes";
type NodeUnion = {
[key in NodeType]: Partial<
NodeMap[key] & {
/**
*
*/
type: key;
}
>;
}[NodeType];
export { nodeMap };
export type { NodeMap, NodeType, NodeUnion };

View File

@ -1,110 +0,0 @@
import { FieldRule, FormInstance, FormItemInstance, SelectOptionData } from "@arco-design/web-vue";
import { InjectionKey, Ref } from "vue";
import { NodeUnion } from "../../nodes";
/**
*
*/
export type FormItemFnArg<T = AppFormItem> = {
item: T;
items: T[];
model: Recordable;
};
/**
*
*/
type BaseFormItem = {
/**
* `FormItem`
* @description fieldlabelrequiredrulesdisabled
*/
itemProps: Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">;
/**
*
* @description
*/
visible?: (arg: FormItemFnArg) => boolean;
/**
*
* @description
*/
disable?: (arg: FormItemFnArg) => boolean;
/**
*
* @description ,
*/
options?: SelectOptionData[] | ((arg: FormItemFnArg) => PromiseLike<Recordable[]>);
};
/**
*
*/
type BaseFormItemSlots = {
/**
*
* @description
*/
render: (args: FormItemFnArg) => any;
/**
*
* @description FormItemlabel
*/
label?: string | ((args: FormItemFnArg) => any);
/**
*
* @description FormItemhelp
*/
help?: string | ((args: FormItemFnArg) => any);
/**
*
* @description FormItemextra
*/
extra?: string | ((args: FormItemFnArg) => any);
};
/**
*
*/
type BaseFormItemRules = {
/**
*
* @description ()
*/
rules?: FieldRule<AppFormItem>[];
};
/**
*
*/
type BaseFormItemModel = {
/**
*
* @example
* ```typescript
* '[v1,v2]' => { v1: 1, v2: 2 }
* ```
*/
field: string;
};
/**
*
*/
export type AppFormItem = BaseFormItem & BaseFormItemModel & BaseFormItemRules & BaseFormItemSlots & NodeUnion;
export type SubmitFn = (arg: { model: Recordable; items: AppFormItem[] }) => PromiseLike<void | { message?: string }>;
interface FormContext {
loading: Ref<boolean>;
formRef: Ref<FormInstance | null>;
submitForm: () => PromiseLike<any>;
}
export const FormKey = Symbol("AppnifyForm") as InjectionKey<FormContext>;

View File

@ -1,14 +0,0 @@
import { InjectionKey } from "vue";
import { FormItems } from "./useFormItems";
import { FormModel } from "./useFormModel";
import { FormRef } from "./useFormRef";
import { FormSubmit } from "./useFormSubmit";
export type FormContextInterface = FormModel &
FormItems &
FormRef &
FormSubmit & {
slots: Recordable;
};
export const FormContextKey = Symbol("FormKey") as InjectionKey<FormContextInterface>;

View File

@ -1,22 +0,0 @@
import { InjectionKey } from "vue";
import { FormItems } from "./useFormItems";
import { FormModel } from "./useFormModel";
import { FormRef } from "./useFormRef";
import { FormSubmit } from "./useFormSubmit";
export type FormContextInterface = FormModel &
FormItems &
FormRef &
FormSubmit & {
slots: Recordable;
};
export const FormContextKey = Symbol("FormContextKey") as InjectionKey<FormContextInterface>;
export function useFormContext() {
const context = inject(FormContextKey);
if (!context) {
throw Error("useFormContext musb be used in AnForm children!");
}
return context;
}

View File

@ -1,46 +0,0 @@
import { Ref } from "vue";
import { IAnFormItem } from "../components/FormItem";
export function useFormItems(items: Ref<IAnFormItem[]>, model: Ref<Recordable>) {
const getItem = (field: string) => {
return items.value.find((i) => i.field === field);
};
const getItemOptions = (field: string) => {
const item = getItem(field);
if (item) {
return (item.nodeProps as any)?.options;
}
};
const initItemOptions = (field: string) => {
const item = getItem(field);
item && item.init?.();
};
const initItems = () => {
for (const item of items.value) {
item.init?.({ item, model: model.value });
}
};
const initItem = (field: string) => {
const item = getItem(field);
item && item.init?.({ item, model: model.value });
};
onMounted(() => {
initItems();
});
return {
items,
getItem,
initItem,
initItems,
getItemOptions,
initItemOptions,
};
}
export type FormItems = ReturnType<typeof useFormItems>;

View File

@ -1,112 +0,0 @@
import { cloneDeep } from "lodash-es";
import { Ref } from "vue";
/**
*
* @param initial
* @returns
*/
export function useFormModel(model: Ref<Recordable>, clearValidate: any) {
const initial = cloneDeep(model.value);
const resetModel = () => {
model.value = cloneDeep(initial);
clearValidate();
};
const getInitialModel = () => {
return initial;
};
const setModel = (data: Recordable) => {
for (const key of Object.keys(model.value)) {
model.value[key] = data[key];
}
};
const getModel = () => {
return formatModel(model.value);
};
return {
model,
getInitialModel,
resetModel,
setModel,
getModel,
};
}
export type FormModel = ReturnType<typeof useFormModel>;
export function formatModel(model: Recordable) {
const data: Recordable = {};
for (const [key, value] of Object.entries(model)) {
if (/^\[.+\]$/.test(key)) {
formatModelArray(key, value, data);
continue;
}
if (/^\{.+\}$/.test(key)) {
formatModelObject(key, value, data);
continue;
}
data[key] = value;
}
return data;
}
function formatModelArray(key: string, value: any, data: Recordable) {
let field = key.replaceAll(/\s/g, "");
field = field.match(/^\[(.+)\]$/)?.[1] ?? "";
if (!field) {
data[key] = value;
return;
}
const keys = field.split(",");
keys.forEach((k, i) => {
if (/(.+)?:number$/.test(k)) {
k = k.replace(/:number$/, "");
data[k] = value?.[i] && Number(value[i]);
return;
}
if (/(.+)?:boolean$/.test(k)) {
k = k.replace(/:boolean$/, "");
data[k] = value?.[i] && Boolean(value[i]);
return;
}
data[k] = value?.[i];
});
return data;
}
function formatModelObject(key: string, value: any, data: Recordable) {
let field = key.replaceAll(/\s/g, "");
field = field.match(/^\{(.+)\}$/)?.[1] ?? "";
if (!field) {
data[key] = value;
return;
}
const keys = field.split(",");
keys.forEach((k, i) => {
if (/(.+)?:number$/.test(k)) {
k = k.replace(/:number$/, "");
data[k] = value?.[i] && Number(value[i]);
return;
}
if (/(.+)?:boolean$/.test(k)) {
k = k.replace(/:boolean$/, "");
data[k] = value?.[i] && Boolean(value[i]);
return;
}
data[k] = value?.[i];
});
return data;
}

View File

@ -1,34 +0,0 @@
import { FormInstance } from "@arco-design/web-vue";
export function useFormRef() {
/**
*
*/
const formRef = ref<FormInstance | null>(null);
type Validate = FormInstance["validate"];
type ValidateField = FormInstance["validateField"];
type ResetFields = FormInstance["resetFields"];
type ClearValidate = FormInstance["clearValidate"];
type SetFields = FormInstance["setFields"];
type ScrollToField = FormInstance["scrollToField"];
const validate: Validate = async (...args) => formRef.value?.validate(...args);
const validateField: ValidateField = async (...args) => formRef.value?.validateField(...args);
const resetFields: ResetFields = (...args) => formRef.value?.resetFields(...args);
const clearValidate: ClearValidate = (...args) => formRef.value?.clearValidate(...args);
const setFields: SetFields = (...args) => formRef.value?.setFields(...args);
const scrollToField: ScrollToField = (...args) => formRef.value?.scrollToField(...args);
return {
formRef,
validate,
validateField,
resetFields,
clearValidate,
setFields,
scrollToField,
};
}
export type FormRef = ReturnType<typeof useFormRef>;

View File

@ -1,57 +0,0 @@
import { FormInstance, Message } from "@arco-design/web-vue";
import { Ref } from "vue";
import { IAnFormItem } from "../components/FormItem";
interface Options {
items: Ref<IAnFormItem[]>;
model: Ref<Recordable>;
submit: Ref<Function | undefined>;
validate: FormInstance["validate"];
}
export function useFormSubmit(options: Options, getModel: any) {
const { items, submit, validate } = options;
const loading = ref(false);
/**
* loading
* @param value
*/
const setLoading = (value: boolean) => {
loading.value = value;
};
/**
*
*/
const submitForm = async () => {
if (await validate()) {
return;
}
try {
loading.value = true;
const data = getModel();
const res = await submit.value?.(data, items.value);
const msg = res?.data?.message;
msg && Message.success(`提示: ${msg}`);
} catch {
console.log();
} finally {
loading.value = false;
}
};
/**
*
*/
const cancelForm = () => {};
return {
loading,
setLoading,
submitForm,
cancelForm,
};
}
export type FormSubmit = ReturnType<typeof useFormSubmit>;

View File

@ -1,25 +0,0 @@
import { FormInstance } from "@arco-design/web-vue";
import { FormItem, FormItemFnArg } from "./FormItem";
type FormInstanceProps = Partial<Omit<FormInstance["$props"], "model">>;
export type UseForm = {
/**
*
*/
model?: Recordable;
/**
*
*/
items?: FormItem[];
/**
*
* @description
*/
submit?: string | ((arg: any) => PromiseLike<any>);
/**
*
* @description
*/
formProps?: FormInstanceProps;
};

View File

@ -1,92 +0,0 @@
import { FormItemInstance } from "@arco-design/web-vue";
import { NodeType, NodeUnion } from "../../nodes";
import { Rule } from "../useRules";
import { IAnFormItem, IAnFormItemBoolFn, IAnFormItemElemFn } from "../../components/FormItem";
/**
*
*/
export type FormItemFnArg<T = FormItem> = {
item: T;
items: T[];
model: Recordable;
};
/**
*
*/
type BaseFormItem = {
/**
*
* @example
* ```typescript
* '[v1, v2]' => { v1, v2 }
* ```
*/
field: string;
/**
*
* @description model
*/
value?: any;
/**
*
* @description
*/
render?: NodeType | IAnFormItemElemFn;
/**
*
* @description FormItemlabel
*/
label?: string | IAnFormItemElemFn;
/**
*
* @description FormItemhelp
*/
help?: string | IAnFormItemElemFn;
/**
*
* @description FormItemextra
*/
extra?: string | IAnFormItemElemFn;
/**
*
* @description false
*/
required?: boolean;
/**
*
* @description ()
*/
rules?: Rule[];
/**
* `FormItem`
* @description fieldlabelrequiredrulesdisabled
*/
itemProps?: Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">;
/**
*
* @description
*/
visible?: IAnFormItemBoolFn;
/**
*
* @description
*/
disable?: IAnFormItemBoolFn;
};
/**
*
*/
export type FormItem = BaseFormItem & NodeUnion;

View File

@ -1,44 +0,0 @@
import { useItems } from "./useItems";
import { FormInstance } from "@arco-design/web-vue";
import { FormItem } from "./types/FormItem";
import { IAnForm, IAnFormSubmit } from "../components/Form";
type FormInstanceProps = Partial<Omit<FormInstance["$props"], "model">>;
export type UseForm = {
/**
*
*/
model?: Recordable;
/**
*
*/
items?: FormItem[];
/**
*
* @description
*/
submit?: IAnFormSubmit;
/**
*
* @description
*/
formProps?: FormInstanceProps;
};
/**
*
*/
export const useForm = (options: UseForm) => {
const { items: _items = [], model: _model = {}, submit, formProps: _props = {} } = options;
const items = useItems(_items, _model, Boolean(options.submit));
const model = ref(_model);
const formProps = ref(_props);
return {
model,
items,
formProps,
submit,
};
};

View File

@ -1,55 +0,0 @@
import { defaultsDeep, merge, omit } from "lodash-es";
import { NodeType, nodeMap } from "../nodes";
import { FormItem } from "./types/FormItem";
import { useRules } from "./useRules";
import { IAnFormItem } from "../components/FormItem";
const ITEM: Partial<FormItem> = {
render: "input",
itemProps: {},
};
const SUBMIT_ITEM: FormItem = {
field: "id",
render: "submit",
itemProps: {
hideLabel: true,
},
};
export function useItems(list: FormItem[], model: Recordable, submit: boolean) {
const items = ref<IAnFormItem[]>([]);
let hasSubmit = false;
for (const item of list) {
let target: any = defaultsDeep({}, ITEM);
if (!item.render || typeof item.render === "string") {
const defaults = nodeMap[item.render ?? "input"];
if (defaults) {
defaultsDeep(target, defaults);
}
}
if (item.render === "submit") {
target = merge(target, SUBMIT_ITEM);
hasSubmit = true;
}
target = merge(target, omit(item, ["required", "rules"]));
const rules = useRules(item);
if (rules) {
target.rules = rules;
}
model[item.field] = model[item.field] ?? item.value;
items.value.push(target);
}
if (submit && !hasSubmit) {
items.value.push(defaultsDeep({}, SUBMIT_ITEM, nodeMap.submit));
}
return items;
}

View File

@ -1,63 +0,0 @@
import { cloneDeep } from "lodash-es";
export function formatModel(model: Recordable) {
const data: Recordable = {};
for (const [key, val] of Object.entries(model)) {
// 数组类型
if (/^\[.+\]$/.test(key)) {
const subkeysStr = key.replaceAll(/\s/g, "").match(/^\[(.+)\]$/)?.[1];
if (!subkeysStr) {
data[key] = val;
continue;
}
const subkeys = subkeysStr.split(",");
subkeys.forEach((subkey, index) => {
if (/(.+)?:number$/.test(subkey)) {
subkey = subkey.replace(/:number$/, "");
data[subkey] = val?.[index] && Number(val[index]);
return;
}
if (/(.+)?:boolean$/.test(subkey)) {
subkey = subkey.replace(/:boolean$/, "");
data[subkey] = val?.[index] && Boolean(val[index]);
return;
}
data[subkey] = val?.[index];
});
continue;
}
// 默认类型
data[key] = val;
}
return data;
}
/**
*
* @param initial
* @returns
*/
export function useModel(initial: Recordable) {
const model = ref<Recordable>({});
const resetModel = () => {
model.value = cloneDeep(initial);
};
const setModel = (data: Recordable) => {
for (const key of Object.keys(model.value)) {
model.value[key] = data[key];
}
};
const getModel = () => {
return formatModel(model.value);
};
return {
model,
resetModel,
setModel,
getModel,
};
}

View File

@ -1,95 +0,0 @@
import { FieldRule } from "@arco-design/web-vue";
import { has, isString } from "lodash-es";
import { IAnFormItemBoolFn, IAnFormItemRule } from "../components/FormItem";
/**
*
*/
export const FieldRuleMap = defineRuleMap({
required: {
required: true,
message: "该项不能为空",
},
string: {
type: "string",
message: "请输入字符串",
},
number: {
type: "number",
message: "请输入数字",
},
email: {
type: "email",
message: "邮箱格式错误,示例: xx@abc.com",
},
url: {
type: "url",
message: "URL格式错误, 示例: www.abc.com",
},
ip: {
type: "ip",
message: "IP格式错误, 示例: 101.10.10.30",
},
phone: {
match: /^(?:(?:\+|00)86)?1\d{10}$/,
message: "手机格式错误, 示例(11位): 15912345678",
},
idcard: {
match: /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/,
message: "身份证格式错误, 长度为15或18位",
},
alphabet: {
match: /^[a-zA-Z]\w{4,15}$/,
message: "请输入英文字母, 长度为4~15位",
},
password: {
match: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/,
message: "至少包含大写字母、小写字母、数字和特殊字符",
},
});
/**
* ()
*/
export type FieldStringRule = keyof typeof FieldRuleMap;
/**
*
*/
export type Rule = FieldStringRule | IAnFormItemRule;
/**
* (TS)
*/
function defineRuleMap<T extends Record<string, FieldRule>>(ruleMap: T) {
return ruleMap;
}
/**
*
* @param item
* @returns
*/
export const useRules = <T extends { required?: boolean; rules?: Rule[] }>(item: T) => {
const { required, rules } = item;
if (!has(item, "required") && !has(item, "rules")) {
return null;
}
const data: IAnFormItemRule[] = [];
if (required) {
data.push(FieldRuleMap.required);
}
for (const rule of rules ?? []) {
if (isString(rule)) {
if (FieldRuleMap[rule]) {
data.push(FieldRuleMap[rule]);
}
} else {
data.push(rule);
}
}
return data;
};

View File

@ -1,14 +0,0 @@
import { Cascader, CascaderInstance } from "@arco-design/web-vue";
import { initOptions } from "../form-config";
type Props = CascaderInstance["$props"];
export default {
render: Cascader,
init: initOptions,
nodeProps: {
placeholder: "请选择",
allowClear: true,
expandTrigger: "hover",
} as Props,
};

View File

@ -1,6 +0,0 @@
export default {
render: () => {
return "1";
},
nodeProps: {},
};

View File

@ -1,11 +0,0 @@
import { DatePicker, DatePickerInstance } from "@arco-design/web-vue";
type Props = DatePickerInstance["$props"];
export default {
render: DatePicker,
nodeProps: {
placeholder: "请选择",
allowClear: true,
} as Props,
};

View File

@ -1,10 +0,0 @@
import { RangePicker, RangePickerInstance } from "@arco-design/web-vue";
type Props = RangePickerInstance["$props"];
export default {
render: RangePicker,
nodeProps: {
allowClear: true,
} as Props,
};

View File

@ -1,11 +0,0 @@
import { Input, InputInstance } from "@arco-design/web-vue";
type Props = InputInstance["$props"];
export default {
render: Input,
nodeProps: {
placeholder: "请输入",
allowClear: true,
} as Props,
};

View File

@ -1,12 +0,0 @@
import { InputInstance, InputNumber, InputNumberInstance } from "@arco-design/web-vue";
type Props = InputInstance["$props"] & InputNumberInstance["$props"];
export default {
render: InputNumber,
nodeProps: {
placeholder: "请输入",
defaultValue: 0,
allowClear: true,
} as Props,
};

View File

@ -1,10 +0,0 @@
import { InputInstance, InputPassword, InputPasswordInstance } from "@arco-design/web-vue";
type Props = InputInstance["$props"] & InputPasswordInstance["$props"];
export default {
render: InputPassword,
nodeProps: {
placeholder: "请输入",
} as Props,
};

View File

@ -1,11 +0,0 @@
import { InputInstance, InputSearch, InputSearchInstance } from "@arco-design/web-vue";
type Props = InputInstance["$props"] & InputSearchInstance["$props"];
export default {
render: InputSearch,
nodeProps: {
placeholder: "请输入",
allowClear: true,
} as Props,
};

View File

@ -1,38 +0,0 @@
import { Select, SelectInstance, SelectOptionData } from "@arco-design/web-vue";
import { initOptions } from "../form-config";
import { Component } from "vue";
import { defineSetter } from "../utils/defineSetter";
interface Interface {
init: any;
render: Component;
nodeProps: SelectInstance["$props"];
/**
*
*/
options?: SelectOptionData[] | ((arg: any) => Recordable[] | Promise<Recordable[]>);
}
const select: Interface = {
render: Select,
init: initOptions,
nodeProps: {
placeholder: "请选择",
allowClear: true,
allowSearch: true,
options: [],
},
options: [],
};
export default select;
// export default defineSetter({
// setter: Select,
// setterProps: {
// placeholder: "请选择",
// allowClear: true,
// allowSearch: true,
// options: [],
// },
// });

View File

@ -1,19 +0,0 @@
import { Button } from "@arco-design/web-vue";
import { FormContextKey } from "../core/interface";
export default {
render() {
const { loading, submitForm, resetModel } = inject(FormContextKey)!;
return (
<>
<Button type="primary" loading={loading.value} onClick={submitForm} class="mr-3">
</Button>
<Button disabled={loading.value} onClick={resetModel}>
</Button>
</>
);
},
nodeProps: {},
};

View File

@ -1,11 +0,0 @@
import { InputInstance, Textarea, TextareaInstance } from "@arco-design/web-vue";
type Props = InputInstance["$props"] & TextareaInstance["$props"];
export default {
render: Textarea,
nodeProps: {
placeholder: "请输入",
allowClear: true,
} as Props,
};

View File

@ -1,10 +0,0 @@
import { TimePicker, TimePickerInstance } from "@arco-design/web-vue";
type Props = TimePickerInstance["$props"];
export default {
render: TimePicker,
nodeProps: {
allowClear: true,
} as Props,
};

View File

@ -1,15 +0,0 @@
import { TreeSelect, TreeSelectInstance } from "@arco-design/web-vue";
import { initOptions } from "../form-config";
type Props = TreeSelectInstance["$props"];
export default {
render: TreeSelect,
init: (arg: any) => initOptions(arg, "data"),
nodeProps: {
placeholder: "请选择",
allowClear: true,
allowSearch: true,
options: [],
} as Props,
};

View File

@ -1,44 +0,0 @@
import cascader from "./Cascader";
import custom from "./Custom";
import date from "./Date";
import input from "./Input";
import number from "./Number";
import password from "./Password";
import search from "./Search";
import select from "./Select";
import submit from "./Submit";
import textarea from "./Textarea";
import time from "./Time";
import treeSelect from "./TreeSelect";
import dateRange from "./DateRange";
export const nodeMap = {
input,
number,
search,
textarea,
select,
treeSelect,
time,
password,
cascader,
date,
submit,
custom,
dateRange,
};
export type NodeMap = typeof nodeMap;
export type NodeType = keyof NodeMap;
export type NodeUnion = {
[key in NodeType]: Partial<
Omit<NodeMap[key], "render"> & {
/**
*
*/
render: key | ((...args: any[]) => any);
}
>;
}[NodeType];

View File

@ -1,27 +0,0 @@
import { Component } from "vue";
import { FormItem } from "../hooks/types/FormItem";
interface Setter<T extends Component, P = T extends new (...args: any) => any ? InstanceType<T>["$props"] : any> {
/**
*
*/
setter: T;
/**
*
*/
setterProps?: P;
/**
*
* @param model
* @param item
* @param items
* @returns
*/
onSetup?: (model: Recordable, item: FormItem, items: FormItem[]) => void;
}
export function defineSetter<T extends Component>(options: Setter<T>): Setter<T> {
return options;
}

View File

@ -1,9 +0,0 @@
export function strOrFnRender(fn: any, options: any) {
if (typeof fn === "string") {
return () => fn;
}
if (typeof fn === "function") {
return fn(options);
}
return null;
}

View File

@ -7,143 +7,8 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { api } from '@/api'; import { api } from '@/api';
import { useTable } from '@/components/AnTable'; import { useTable } from '@/components/AnTable';
import { useColumnConfig } from '@/components/AnTable/plugins/useColumnConfig';
import { useRefresh } from '@/components/AnTable/plugins/useRefreshPlugin';
import { useSelection } from '@/components/AnTable/plugins/useSelectionPlugin';
import { delConfirm, sleep } from '@/utils';
import { Button, Message } from '@arco-design/web-vue';
import { Ref } from 'vue';
const { component: UserTable } = useTable({ const { component: UserTable } = useTable({
plugins: [
useRefresh(),
useColumnConfig(),
(() => {
let selected: Ref<any[]>;
return {
id: 'deletemany',
options(options: any) {
let selectPlugin = options.plugins.find((i: any) => i.id === 'selection');
if (!selectPlugin) {
selectPlugin = useSelection();
options.plugins.push(selectPlugin);
}
selected = selectPlugin.provide.selected;
return options;
},
action() {
const loading = ref(false);
const onClick = async () => {
await delConfirm();
loading.value = true;
await sleep(3000);
loading.value = false;
selected.value = [];
Message.success('提示: 删除成功!');
};
return () => (
<Button status="danger" disabled={!selected.value.length} loading={loading.value} onClick={onClick}>
{{
icon: () => <i class="icon-park-outline-delete" />,
default: () => '删除',
}}
</Button>
);
},
};
})(),
(() => {
let selected: Ref<any[]>;
return {
id: 'export',
options(options: any) {
let selectPlugin = options.plugins.find((i: any) => i.id === 'selection');
if (!selectPlugin) {
selectPlugin = useSelection();
options.plugins.push(selectPlugin);
}
selected = selectPlugin.provide.selected;
return options;
},
action() {
const onClick = async () => {
await delConfirm('确认导出选中数据吗?');
await sleep(3000);
selected.value = [];
Message.success('提示: 删除成功!');
};
return () => (
<Button disabled={!selected.value.length} onClick={onClick}>
{{
icon: () => <i class="icon-park-outline-export"></i>,
default: () => '导出',
}}
</Button>
);
},
};
})(),
(() => {
return {
id: 'import',
action() {
const onClick = async () => {
Message.success('提示: TODO!');
};
return () => (
<Button onClick={onClick}>
{{
icon: () => <i class="icon-park-outline-code-download"></i>,
default: () => '导入',
}}
</Button>
);
},
};
})(),
(() => {
return {
id: 'format',
options(options) {
for (const column of options.columns ?? []) {
if (column.render) {
continue;
}
column.render = ({ record, column }) => record[column.dataIndex!] ?? '-';
}
},
};
})(),
(() => {
return {
id: 'rowDelete',
options(options) {
for (const column of options.columns ?? []) {
if (column.type !== 'button') {
continue;
}
const btn = column.buttons.find(i => i.type === 'delete');
if (!btn) {
continue;
}
const onClick = btn.onClick;
btn.onClick = async props => {
await delConfirm(btn.confirm);
const res: any = await onClick?.(props);
const msg = res?.data?.message;
msg && Message.success(`提示: ${msg}`);
};
}
},
};
})(),
],
data(search) {
return api.user.getUsers(search);
},
paging: {
hide: false,
},
columns: [ columns: [
{ {
dataIndex: 'id', dataIndex: 'id',
@ -189,21 +54,31 @@ const { component: UserTable } = useTable({
configable: false, configable: false,
buttons: [ buttons: [
{ {
type: 'modify',
text: '修改', text: '修改',
}, },
{ {
text: '移动', text: '移动',
// visible: () => false, disable: () => true,
}, },
{ {
type: 'delete', type: 'delete',
confirm: '确定删除吗?', confirm: '确定删除吗?',
text: '删除', text: '删除',
// disable: () => true, onClick(props) {
const id = props.record.id;
return api.user.delUser(id);
},
}, },
], ],
}, },
], ],
source: search => {
return api.user.getUsers(search);
},
paging: {
hide: false,
},
search: [ search: [
{ {
field: 'username', field: 'username',
@ -213,9 +88,7 @@ const { component: UserTable } = useTable({
], ],
create: { create: {
title: '新增', title: '新增',
modalProps: { width: 580,
width: 580,
},
items: [ items: [
{ {
field: 'title', field: 'title',
@ -239,6 +112,7 @@ const { component: UserTable } = useTable({
}, },
modify: { modify: {
extend: true, extend: true,
title: '修改',
}, },
}); });
</script> </script>

View File

@ -19,108 +19,100 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { DictType, api } from "@/api"; import { DictType, api } from '@/api';
import { createColumn, updateColumn, useAniTable } from "@/components"; import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
import aniGroup from "./components/group.vue"; import aniGroup from './components/group.vue';
defineOptions({ name: "SystemDictPage" })
defineOptions({ name: 'SystemDictPage' });
const current = ref<DictType>(); const current = ref<DictType>();
const onTypeChange = (item: DictType) => { const onTypeChange = (item: DictType) => {
current.value = item; current.value = item;
dict.refresh(); tableRef.value?.refresh();
}; };
const [dictTable, dict] = useAniTable({ const { component: DictTable, tableRef } = useTable({
async data(search, paging) {
return api.dict.getDicts({ ...search, ...paging, typeId: current.value?.id } as any);
},
columns: [ columns: [
{ {
title: "字典项", title: '字典项',
dataIndex: "name", dataIndex: 'name',
render: ({ record }) => ( render: ({ record }) => (
<div> <div>
<div> <div>
{record.name}<span class="text-gray-400 ml-2 text-xs">{record.code}</span> {record.name}
<span class="text-gray-400 ml-2 text-xs">{record.code}</span>
</div> </div>
<div class="text-gray-400 text-xs">{record.description}</div> <div class="text-gray-400 text-xs">{record.description}</div>
</div> </div>
), ),
}, },
createColumn, useCreateColumn(),
updateColumn, useUpdateColumn(),
{ {
title: "操作", title: '操作',
type: "button", type: 'button',
width: 140, width: 140,
buttons: [ buttons: [
{ {
type: "modify", type: 'modify',
text: "修改", text: '修改',
}, },
{ {
type: "delete", type: 'delete',
text: "删除", text: '删除',
onClick: ({ record }) => { onClick: props => {
return api.dict.delDict(record.id); return api.dict.delDict(props.record.id);
}, },
}, },
], ],
}, },
], ],
source(search) {
return api.dict.getDicts({ ...search, typeId: current.value?.id } as any);
},
search: { search: {
button: false, hideSearch: true,
items: [ items: [
{ {
field: "name", field: 'name',
label: "名称", label: '名称',
type: "search", setter: 'search',
searchable: true, searchable: true,
enterable: true, enterable: true,
nodeProps: {
placeholder: "字典名称",
},
itemProps: {
hideLabel: true,
},
}, },
], ],
}, },
create: { create: {
title: "新增字典", title: '新增字典',
width: 580,
model: { model: {
typeId: undefined, typeId: undefined,
}, },
modalProps: {
width: 580,
},
items: [ items: [
{ {
field: "name", field: 'name',
label: "字典名", label: '字典名',
type: "input", setter: 'input',
}, },
{ {
field: "code", field: 'code',
label: "字典值", label: '字典值',
type: "input", setter: 'input',
}, },
{ {
field: "description", field: 'description',
label: "备注", label: '备注',
type: "textarea", setter: 'textarea',
}, },
], ],
submit: async ({ model }) => { submit: model => {
return api.dict.addDict({ ...model, typeId: current.value?.id }); return api.dict.addDict({ ...model, typeId: current.value?.id } as any);
}, },
}, },
modify: { modify: {
extend: true, extend: true,
title: "修改字典", title: '修改字典',
submit: async ({ model }) => { submit: model => {
return api.dict.setDict(model.id, { ...model, typeId: current.value?.id }); return api.dict.setDict(model.id, { ...model, typeId: current.value?.id } as any);
}, },
}, },
}); });

View File

@ -7,13 +7,17 @@ export {}
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
AAlert: typeof import('@arco-design/web-vue')['Alert']
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete'] AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
AAvatar: typeof import('@arco-design/web-vue')['Avatar'] AAvatar: typeof import('@arco-design/web-vue')['Avatar']
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb'] ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem'] ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
AButton: typeof import('@arco-design/web-vue')['Button'] AButton: typeof import('@arco-design/web-vue')['Button']
ACard: typeof import('@arco-design/web-vue')['Card']
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox'] ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'] AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADivider: typeof import('@arco-design/web-vue')['Divider'] ADivider: typeof import('@arco-design/web-vue')['Divider']
ADoption: typeof import('@arco-design/web-vue')['Doption'] ADoption: typeof import('@arco-design/web-vue')['Doption']
ADrawer: typeof import('@arco-design/web-vue')['Drawer'] ADrawer: typeof import('@arco-design/web-vue')['Drawer']
@ -22,6 +26,7 @@ declare module '@vue/runtime-core' {
AEmpty: typeof import('@arco-design/web-vue')['Empty'] AEmpty: typeof import('@arco-design/web-vue')['Empty']
AForm: typeof import('@arco-design/web-vue')['Form'] AForm: typeof import('@arco-design/web-vue')['Form']
AFormItem: typeof import('@arco-design/web-vue')['FormItem'] AFormItem: typeof import('@arco-design/web-vue')['FormItem']
AImage: typeof import('@arco-design/web-vue')['Image']
AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview'] AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview']
AInput: typeof import('@arco-design/web-vue')['Input'] AInput: typeof import('@arco-design/web-vue')['Input']
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber'] AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
@ -32,6 +37,9 @@ declare module '@vue/runtime-core' {
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader'] ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider'] ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
ALink: typeof import('@arco-design/web-vue')['Link'] ALink: typeof import('@arco-design/web-vue')['Link']
AList: typeof import('@arco-design/web-vue')['List']
AListItem: typeof import('@arco-design/web-vue')['ListItem']
AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta']
AMenu: typeof import('@arco-design/web-vue')['Menu'] AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem'] AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal'] AModal: typeof import('@arco-design/web-vue')['Modal']
@ -46,9 +54,12 @@ declare module '@vue/runtime-core' {
ASpace: typeof import('@arco-design/web-vue')['Space'] ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin'] ASpin: typeof import('@arco-design/web-vue')['Spin']
ASwitch: typeof import('@arco-design/web-vue')['Switch'] ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag'] ATag: typeof import('@arco-design/web-vue')['Tag']
ATextarea: typeof import('@arco-design/web-vue')['Textarea'] ATextarea: typeof import('@arco-design/web-vue')['Textarea']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip'] ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
ATree: typeof import('@arco-design/web-vue')['Tree']
AUpload: typeof import('@arco-design/web-vue')['Upload'] AUpload: typeof import('@arco-design/web-vue')['Upload']
BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default'] BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default']
BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default'] BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default']

View File

@ -1,29 +1,27 @@
import { Modal, ModalConfig } from "@arco-design/web-vue"; import { Modal, ModalConfig } from '@arco-design/web-vue';
import { merge } from "lodash-es"; import { merge } from 'lodash-es';
type DelOptions = string | Partial<Omit<ModalConfig, "onOk" | "onCancel">>; export type DelOptions = string | Partial<Omit<ModalConfig, 'onOk' | 'onCancel'>>;
const delOptions = { export const delOptions: ModalConfig = {
title: "提示", title: '提示',
titleAlign: "start", titleAlign: 'start',
width: 432, width: 432,
content: "危险操作,确定删除该数据吗?", content: '危险操作,确定删除该数据吗?',
maskClosable: false, maskClosable: false,
closable: false, closable: false,
okText: "确定", okText: '确定',
okButtonProps: { okButtonProps: {
status: "danger", status: 'danger',
}, },
}; };
const delConfirm = (config: DelOptions = {}) => { export const delConfirm = (config: DelOptions = {}) => {
if (typeof config === "string") { if (typeof config === 'string') {
config = { content: config }; config = { content: config };
} }
const merged = merge({}, delOptions, config); const merged = merge({ content: '' }, delOptions, config);
return new Promise<void>((onOk: () => void, onCancel) => { return new Promise<void>((onOk: () => void, onCancel) => {
Modal.open({ ...merged, onOk, onCancel }); Modal.open({ ...merged, onOk, onCancel });
}); });
}; };
export { delConfirm };