Compare commits
9 Commits
34b3a73f30
...
2b5c367117
| Author | SHA1 | Date |
|---|---|---|
|
|
2b5c367117 | |
|
|
1f7c1a95b3 | |
|
|
85b781d946 | |
|
|
17c695d065 | |
|
|
eeed362320 | |
|
|
72fd7eba25 | |
|
|
51e287c747 | |
|
|
71baafecc7 | |
|
|
a2c263cef7 |
4
.env
4
.env
|
|
@ -2,13 +2,13 @@
|
|||
# 应用配置
|
||||
# =====================================================================================
|
||||
# 网站标题
|
||||
VITE_TITLE = 绝弹项目管理
|
||||
VITE_TITLE = Appnify
|
||||
# 网站副标题
|
||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||
# 部署路径: 当为 ./ 时路由模式需为 hash
|
||||
VITE_BASE = /
|
||||
# 接口前缀:参见 axios 的 baseURL
|
||||
VITE_API = /
|
||||
VITE_API = https://appnify.app.juetan.cn/
|
||||
# 首页路径
|
||||
VITE_HOME_PATH = /home/home
|
||||
# 路由模式:web(路径) hash(锚点)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true
|
||||
"bracketSpacing": true,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ declare module "axios" {
|
|||
* @private
|
||||
*/
|
||||
closeToast?: () => void;
|
||||
/**
|
||||
* 是否在请求成功后提示
|
||||
* @default false
|
||||
*/
|
||||
msg?: boolean | string;
|
||||
/**
|
||||
* 响应异常提示
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { IToastOptions, toast } from "@/components";
|
||||
import { AxiosInstance } from "axios";
|
||||
import { IToastOptions, toast } from '@/components';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { AxiosInstance } from 'axios';
|
||||
|
||||
/**
|
||||
* 提示拦截器
|
||||
|
|
@ -7,31 +8,35 @@ import { AxiosInstance } from "axios";
|
|||
*/
|
||||
export function addToastInterceptor(axios: AxiosInstance) {
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
config => {
|
||||
if (config.toast) {
|
||||
let options: IToastOptions = {};
|
||||
if (typeof config.toast === "string") {
|
||||
if (typeof config.toast === 'string') {
|
||||
options = { message: config.toast };
|
||||
}
|
||||
if (typeof config.toast === "object") {
|
||||
if (typeof config.toast === 'object') {
|
||||
options = config.toast;
|
||||
}
|
||||
config.closeToast = toast(options);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
error => {
|
||||
error.config.closeToast?.();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
response.config.closeToast?.();
|
||||
response => {
|
||||
const { closeToast, msg } = response.config;
|
||||
closeToast?.();
|
||||
if (msg) {
|
||||
Message.success(`提示: ${typeof msg === 'string' ? msg : response.data?.message}`);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
error => {
|
||||
error.config.closeToast?.();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { Form, FormInstance } from '@arco-design/web-vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { PropType } from 'vue';
|
||||
import { FormContextKey } from './useFormContext';
|
||||
import { useFormItems } from './useFormItems';
|
||||
import { useFormModel } from './useFormModel';
|
||||
import { useFormRef } from './useFormRef';
|
||||
import { useFormSubmit } from './useFormSubmit';
|
||||
import { AnFormItem, IAnFormItem } from './FormItem';
|
||||
|
||||
/**
|
||||
* 表单组件
|
||||
*/
|
||||
export const AnForm = defineComponent({
|
||||
name: 'AnForm',
|
||||
props: {
|
||||
/**
|
||||
* 表单数据
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* id: undefined
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
model: {
|
||||
type: Object as PropType<Recordable>,
|
||||
default: () => ({}),
|
||||
},
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
items: {
|
||||
type: Array as PropType<IAnFormItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* 提交表单
|
||||
* @example
|
||||
* ```ts
|
||||
* (model) => api.user.addUser(model)
|
||||
* ```
|
||||
*/
|
||||
submit: {
|
||||
type: [String, Function, Object] as PropType<IAnFormSubmit>,
|
||||
},
|
||||
/**
|
||||
* 传给Form组件的参数
|
||||
* @exmaple
|
||||
* ```ts
|
||||
* {
|
||||
* layout: 'vertical'
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
formProps: {
|
||||
type: Object as IAnFormProps,
|
||||
},
|
||||
},
|
||||
emits: ['update:model'],
|
||||
setup(props, { slots, emit }) {
|
||||
const model = useVModel(props, 'model', emit);
|
||||
const items = computed(() => props.items);
|
||||
const formRefes = useFormRef();
|
||||
const formModel = useFormModel(model, formRefes.clearValidate);
|
||||
const formItems = useFormItems(items, model);
|
||||
const formSubmit = useFormSubmit(props, formRefes.validate, formModel.getModel);
|
||||
const context = { slots, ...formModel, ...formItems, ...formRefes, ...formSubmit };
|
||||
|
||||
provide(FormContextKey, context);
|
||||
return context;
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<Form 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>
|
||||
))}
|
||||
{this.$slots.submit?.(this.model, this.validate) ||
|
||||
(this.submit && this.submitItem && (
|
||||
<AnFormItem item={this.submitItem} items={this.items} model={this.model}></AnFormItem>
|
||||
))}
|
||||
</Form>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export type AnFormInstance = InstanceType<typeof AnForm>;
|
||||
|
||||
export type AnFormProps = AnFormInstance['$props'];
|
||||
|
||||
export type IAnFormProps = PropType<Omit<FormInstance['$props'], 'model'>>;
|
||||
|
||||
export type IAnForm = Pick<AnFormProps, 'model' | 'items' | 'submit' | 'formProps'>;
|
||||
|
||||
export type IAnFormSubmitFn = (model: Recordable, items: IAnFormItem[]) => any;
|
||||
|
||||
export type IAnFormSubmit = string | IAnFormSubmitFn;
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import {
|
||||
FormItem as BaseFormItem,
|
||||
FieldRule,
|
||||
FormItemInstance,
|
||||
SelectOptionData,
|
||||
SelectOptionGroup,
|
||||
} from '@arco-design/web-vue';
|
||||
import { InjectionKey, PropType, provide } from 'vue';
|
||||
import { SetterItem, SetterType, setterMap } from './FormSetter';
|
||||
|
||||
export const FormItemContextKey = Symbol('FormItemContextKey') as InjectionKey<IAnFormItemFnProps>;
|
||||
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
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)));
|
||||
const disabled = computed(() => Boolean(props.item.disable?.(props)));
|
||||
|
||||
const setterSlots = (() => {
|
||||
const slots = props.item.setterSlots;
|
||||
if (!slots) {
|
||||
return null;
|
||||
}
|
||||
const items: Recordable = {};
|
||||
for (const [name, Slot] of Object.entries(slots)) {
|
||||
items[name] = (p: Recordable) => <Slot {...p} {...props} />;
|
||||
}
|
||||
return items;
|
||||
})();
|
||||
|
||||
const itemSlots = (() => {
|
||||
const Setter = setterMap[props.item.setter as SetterType]?.setter as any;
|
||||
const slots = props.item.itemSlots;
|
||||
if (!slots && !Setter) {
|
||||
return null;
|
||||
}
|
||||
const SetterRender = () => (
|
||||
<Setter {...props.item.setterProps} v-model={props.model[props.item.field]}>
|
||||
{setterSlots}
|
||||
</Setter>
|
||||
);
|
||||
if (!slots) {
|
||||
return {
|
||||
default: SetterRender,
|
||||
};
|
||||
}
|
||||
const items: Recordable = {};
|
||||
for (const [name, Slot] of Object.entries(slots)) {
|
||||
items[name] = (p: Recordable) => <Slot {...p} {...props}></Slot>;
|
||||
}
|
||||
if (Setter) {
|
||||
items.default = SetterRender;
|
||||
}
|
||||
return items;
|
||||
})();
|
||||
|
||||
provide(FormItemContextKey, props);
|
||||
|
||||
return () => {
|
||||
if (props.item.visible && !props.item.visible(props)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<BaseFormItem
|
||||
{...props.item.itemProps}
|
||||
label={props.item.label as string}
|
||||
rules={rules.value}
|
||||
disabled={disabled.value}
|
||||
field={props.item.field}
|
||||
>
|
||||
{itemSlots}
|
||||
</BaseFormItem>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export type IAnFormItemBoolFn = (args: IAnFormItemFnProps) => boolean;
|
||||
|
||||
export type IAnFormItemElemFn = (args: IAnFormItemFnProps) => any;
|
||||
|
||||
export type IAnFormItemFnProps = { model: Recordable; item: IAnFormItem; items: IAnFormItem[] };
|
||||
|
||||
export type IAnFormItemRule = FieldRule & { disable?: IAnFormItemBoolFn };
|
||||
|
||||
export type IAnFormItemOption = string | number | boolean | SelectOptionData | SelectOptionGroup;
|
||||
|
||||
export type IAnFormItemSlot = (props: IAnFormItemFnProps) => any;
|
||||
|
||||
export type IAnFormItemSlots = {
|
||||
/**
|
||||
* 默认插槽
|
||||
* @param props 参数
|
||||
* @returns
|
||||
*/
|
||||
default?: IAnFormItemSlot;
|
||||
/**
|
||||
* 帮助插槽
|
||||
* @param props 参数
|
||||
* @returns
|
||||
*/
|
||||
help?: IAnFormItemSlot;
|
||||
/**
|
||||
* 额外插槽
|
||||
* @param props 参数
|
||||
* @returns
|
||||
*/
|
||||
extra?: IAnFormItemSlot;
|
||||
/**
|
||||
* 标签插槽
|
||||
* @param props 参数
|
||||
* @returns
|
||||
*/
|
||||
label?: IAnFormItemSlot;
|
||||
};
|
||||
|
||||
export type IAnFormItemBase = {
|
||||
/**
|
||||
* 字段名
|
||||
* @description 字段名唯一,支持特殊语法
|
||||
* @example
|
||||
* ```ts
|
||||
* 'username'
|
||||
* ```
|
||||
*/
|
||||
field: string;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
* @example
|
||||
* ```ts
|
||||
* '昵称'
|
||||
* ```
|
||||
*/
|
||||
label?: string;
|
||||
|
||||
/**
|
||||
* 校验规则
|
||||
* @example ['email']
|
||||
*/
|
||||
rules?: IAnFormItemRule[];
|
||||
|
||||
/**
|
||||
* 是否可见
|
||||
* @example
|
||||
* ```ts
|
||||
* (props) => Boolean(props.model.id)
|
||||
* ```
|
||||
*/
|
||||
visible?: IAnFormItemBoolFn;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
* @example
|
||||
* ```ts
|
||||
* (props) => Boolean(props.model.id)
|
||||
* ```
|
||||
*/
|
||||
disable?: IAnFormItemBoolFn;
|
||||
|
||||
/**
|
||||
* 选项
|
||||
* @description 适用于下拉框等组件
|
||||
* @example
|
||||
* ```ts
|
||||
* [{ label: '方式1', value: 1 }]
|
||||
* ```
|
||||
*/
|
||||
options?: IAnFormItemOption[] | ((args: IAnFormItemFnProps) => IAnFormItemOption[] | Promise<IAnFormItemOption[]>);
|
||||
|
||||
/**
|
||||
* 表单项参数
|
||||
* @default null
|
||||
*/
|
||||
itemProps?: Partial<Omit<FormItemInstance['$props'], 'field' | 'label' | 'required' | 'rules' | 'disabled'>>;
|
||||
|
||||
/**
|
||||
* 表单项插槽
|
||||
* @see 1
|
||||
*/
|
||||
itemSlots?: IAnFormItemSlots;
|
||||
|
||||
/**
|
||||
* 内置
|
||||
* @private
|
||||
*/
|
||||
$init?: () => void;
|
||||
};
|
||||
|
||||
export type IAnFormItem = IAnFormItemBase & SetterItem;
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { useVisible } from "@/hooks/useVisible";
|
||||
import { Button, ButtonInstance, Modal } from "@arco-design/web-vue";
|
||||
import { PropType } from "vue";
|
||||
import { useModalSubmit } from "./useModalSubmit";
|
||||
import { useModalTrigger } from "./useModalTrigger";
|
||||
import { AnForm, IAnFormProps, IAnFormSubmit } from "./Form";
|
||||
import { IAnFormItem } from "./FormItem";
|
||||
|
||||
/**
|
||||
* 表单组件
|
||||
*/
|
||||
export const AnFormModal = defineComponent({
|
||||
name: "AnFormModal",
|
||||
props: {
|
||||
/**
|
||||
* 弹窗标题
|
||||
* @default '新增'
|
||||
*/
|
||||
title: {
|
||||
type: [String, Function] as PropType<ModalType>,
|
||||
default: "新增",
|
||||
},
|
||||
/**
|
||||
* 触发元素
|
||||
* @default '新增'
|
||||
*/
|
||||
trigger: {
|
||||
type: [Boolean, String, Function, Object] as PropType<ModalTrigger>,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 传递给Modal的props
|
||||
*/
|
||||
modalProps: {
|
||||
type: Object as PropType<ModalProps>,
|
||||
},
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
model: {
|
||||
type: Object as PropType<Recordable>,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
items: {
|
||||
type: Array as PropType<IAnFormItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
submit: {
|
||||
type: [String, Function] as PropType<IAnFormSubmit>,
|
||||
},
|
||||
/**
|
||||
* 传给Form组件的参数
|
||||
*/
|
||||
formProps: {
|
||||
type: Object as IAnFormProps,
|
||||
},
|
||||
},
|
||||
emits: ["update:model"],
|
||||
setup(props, { slots, emit }) {
|
||||
const formRef = ref<InstanceType<typeof AnForm> | null>(null);
|
||||
const { visible, show, hide } = useVisible();
|
||||
const { modalTrigger } = useModalTrigger(props, show);
|
||||
const { loading, setLoading, submitForm } = useModalSubmit(props, formRef, visible);
|
||||
|
||||
const open = (data: Recordable = {}) => {
|
||||
formRef.value?.setModel(data);
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setLoading(false);
|
||||
hide();
|
||||
};
|
||||
|
||||
const onClose = () => {};
|
||||
|
||||
const modalTitle = () => {
|
||||
if (typeof props.title === "string") {
|
||||
return props.title;
|
||||
}
|
||||
return <props.title model={props.model} items={props.items}></props.title>;
|
||||
};
|
||||
|
||||
return {
|
||||
visible,
|
||||
loading,
|
||||
formRef,
|
||||
open,
|
||||
close,
|
||||
submitForm,
|
||||
modalTitle,
|
||||
modalTrigger,
|
||||
onClose,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<this.modalTrigger></this.modalTrigger>
|
||||
<Modal
|
||||
titleAlign="start"
|
||||
closable={false}
|
||||
{...this.$attrs}
|
||||
{...this.modalProps}
|
||||
maskClosable={false}
|
||||
onClose={this.onClose}
|
||||
v-model:visible={this.visible}
|
||||
>
|
||||
{{
|
||||
title: this.modalTitle,
|
||||
default: () => (
|
||||
<AnForm
|
||||
ref="formRef"
|
||||
model={this.model}
|
||||
onUpdate:model={(v) => this.$emit("update:model", v)}
|
||||
items={this.items}
|
||||
formProps={this.formProps}
|
||||
></AnForm>
|
||||
),
|
||||
footer: () => (
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div></div>
|
||||
<div class="space-x-2">
|
||||
<Button onClick={() => (this.visible = false)}>取消</Button>
|
||||
<Button type="primary" loading={this.loading} onClick={this.submitForm}>
|
||||
确定
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
type ModalProps = Partial<Omit<InstanceType<typeof Modal>["$props"], "visible" | "title" | "onBeforeOk">>;
|
||||
|
||||
type ModalType = string | ((model: Recordable, items: IAnFormItem[]) => any);
|
||||
|
||||
type ModalTrigger =
|
||||
| boolean
|
||||
| string
|
||||
| ((model: Recordable, items: IAnFormItem[]) => any)
|
||||
| {
|
||||
text?: string;
|
||||
buttonProps?: ButtonInstance["$props"];
|
||||
buttonSlots?: Recordable;
|
||||
};
|
||||
|
||||
export type FormModalProps = Pick<
|
||||
InstanceType<typeof AnFormModal>["$props"],
|
||||
"title" | "trigger" | "modalProps" | "model" | "items" | "submit" | "formProps"
|
||||
>;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import setterMap from '../setters';
|
||||
|
||||
export type SetterMap = typeof setterMap;
|
||||
|
||||
export type SetterType = keyof SetterMap;
|
||||
|
||||
export type SetterItemMap = {
|
||||
[key in SetterType]: {
|
||||
/**
|
||||
* 控件类型
|
||||
* @example
|
||||
* ```ts
|
||||
* 'input'
|
||||
* ```
|
||||
*/
|
||||
setter: key;
|
||||
/**
|
||||
* 控件参数
|
||||
* @example
|
||||
* ```tsx
|
||||
* { type: "password" }
|
||||
* ```
|
||||
*/
|
||||
setterProps?: SetterMap[key]['setterProps'];
|
||||
/**
|
||||
* 控件插槽
|
||||
* @example
|
||||
* ```tsx
|
||||
* label: (props) => <span>{props.item.label}</span>
|
||||
* ```
|
||||
*/
|
||||
setterSlots?: SetterMap[key]['setterSlots'];
|
||||
};
|
||||
};
|
||||
|
||||
export type SetterItem = SetterItemMap[SetterType] | { setter?: undefined; setterProps?: undefined; setterSlots?: undefined };
|
||||
|
||||
export { setterMap };
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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>;
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { Ref } from 'vue';
|
||||
import { IAnFormItem } from './FormItem';
|
||||
import { setterMap } from './FormSetter';
|
||||
|
||||
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.setterProps as any)?.options;
|
||||
}
|
||||
};
|
||||
|
||||
const initItemOptions = (field: string) => {
|
||||
const item = getItem(field);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const setter = setterMap[item.setter!];
|
||||
if (!setter) {
|
||||
return;
|
||||
}
|
||||
setter.onSetup?.({ item, items: items.value, model: model.value });
|
||||
};
|
||||
|
||||
const initItems = () => {
|
||||
for (const item of items.value) {
|
||||
const setter = setterMap[item?.setter!];
|
||||
setter.onSetup?.({ item, items: items.value, model: model.value });
|
||||
}
|
||||
};
|
||||
|
||||
const initItem = (field: string) => {
|
||||
const item = getItem(field);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const setter = setterMap[item?.setter!];
|
||||
setter.onSetup?.({ item, items: items.value, model: model.value });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initItems();
|
||||
});
|
||||
|
||||
return {
|
||||
items,
|
||||
getItem,
|
||||
initItem,
|
||||
initItems,
|
||||
getItemOptions,
|
||||
initItemOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export type FormItems = ReturnType<typeof useFormItems>;
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
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 (value === '') {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
field.split(',').forEach((key, index) => {
|
||||
data[key] = value?.[index];
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for (const key of field.split(',')) {
|
||||
data[key] = value?.[key];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
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>;
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { Message } from '@arco-design/web-vue';
|
||||
import { IAnForm } from './Form';
|
||||
import { IAnFormItem } from './FormItem';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
const SUBMIT_ITEM = {
|
||||
field: 'id',
|
||||
setter: 'submit' as const,
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
};
|
||||
|
||||
export function useFormSubmit(props: IAnForm, validate: any, getModel: any) {
|
||||
const loading = ref(false);
|
||||
const submitItem = ref<IAnFormItem | null>(null);
|
||||
|
||||
if (props.submit) {
|
||||
submitItem.value = cloneDeep(SUBMIT_ITEM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置loading
|
||||
* @param value 值
|
||||
*/
|
||||
const setLoading = (value: boolean) => {
|
||||
loading.value = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const submitForm = async () => {
|
||||
if (await validate()) {
|
||||
return;
|
||||
}
|
||||
const submit = typeof props.submit === 'string' ? () => null : props.submit;
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = getModel();
|
||||
const res = await submit?.(data, props.items ?? []);
|
||||
const msg = res?.data?.message;
|
||||
msg && Message.success(`提示: ${msg}`);
|
||||
} catch {
|
||||
console.log();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消提交
|
||||
*/
|
||||
const cancelForm = () => {};
|
||||
|
||||
return {
|
||||
loading,
|
||||
submitItem,
|
||||
setLoading,
|
||||
submitForm,
|
||||
cancelForm,
|
||||
};
|
||||
}
|
||||
|
||||
export type FormSubmit = ReturnType<typeof useFormSubmit>;
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { sleep } from "@/utils";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { Ref } from "vue";
|
||||
|
||||
export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>) {
|
||||
const loading = ref(false);
|
||||
|
||||
const submitForm = async () => {
|
||||
if (await formRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = formRef.value?.getModel() ?? {};
|
||||
await sleep(5000);
|
||||
const res = await props.submit?.(data, props.items);
|
||||
const msg = res?.data?.message;
|
||||
msg && Message.success(msg);
|
||||
visible.value = false;
|
||||
} catch {
|
||||
// todo
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setLoading = (value: boolean) => {
|
||||
loading.value = value;
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
setLoading,
|
||||
submitForm,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { Button } from "@arco-design/web-vue";
|
||||
|
||||
export function useModalTrigger(props: any, open: () => void) {
|
||||
const modalTrigger = () => {
|
||||
if (!props.trigger) {
|
||||
return null;
|
||||
}
|
||||
if (typeof props.trigger === "function") {
|
||||
return <props.trigger model={props.model} items={props.items} open={open}></props.trigger>;
|
||||
}
|
||||
const internal = {
|
||||
text: "新增",
|
||||
buttonProps: {},
|
||||
buttonSlots: {},
|
||||
};
|
||||
if (typeof props.trigger === "string") {
|
||||
internal.text = props.trigger;
|
||||
}
|
||||
if (typeof props.trigger === "object") {
|
||||
Object.assign(internal, props.trigger);
|
||||
}
|
||||
return (
|
||||
<Button type="primary" {...internal.buttonProps} onClick={open}>
|
||||
{{
|
||||
...internal.buttonSlots,
|
||||
default: () => internal.text,
|
||||
}}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
return { modalTrigger };
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { AnForm, IAnForm } from '../components/Form';
|
||||
import { FormItem, useItems } from './useItems';
|
||||
|
||||
export type FormUseOptions = Partial<Omit<IAnForm, 'items'>> & {
|
||||
/**
|
||||
* 表单项
|
||||
* @example
|
||||
* ```ts
|
||||
* [{
|
||||
* field: 'name',
|
||||
* label: '昵称',
|
||||
* setter: 'input'
|
||||
* }]
|
||||
* ```
|
||||
*/
|
||||
items?: FormItem[];
|
||||
};
|
||||
|
||||
export function useFormProps(options: FormUseOptions) {
|
||||
const _model = options.model ?? {};
|
||||
const _items = options.items ?? [];
|
||||
const items = useItems(_items, _model);
|
||||
const props = reactive({
|
||||
formProps: options.formProps ?? {},
|
||||
items: items.value,
|
||||
submit: options.submit,
|
||||
model: _model,
|
||||
});
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建表单组件的参数
|
||||
*/
|
||||
export const useForm = (options: FormUseOptions) => {
|
||||
const { items: _items = [], model: _model = {}, submit, formProps: _props = {} } = options;
|
||||
const items = useItems(_items, _model);
|
||||
const model = ref(_model);
|
||||
const formProps = ref(_props);
|
||||
const formRef = ref<InstanceType<typeof AnForm> | null>(null);
|
||||
|
||||
const AnFormer = () => (
|
||||
<AnForm
|
||||
ref={(el: any) => (formRef.value = el)}
|
||||
v-model:model={model.value}
|
||||
items={items.value}
|
||||
submit={submit}
|
||||
formProps={formProps.value}
|
||||
></AnForm>
|
||||
);
|
||||
|
||||
return {
|
||||
component: AnFormer,
|
||||
model,
|
||||
items,
|
||||
submit,
|
||||
formProps,
|
||||
formRef,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { AnFormModal, FormModalProps } from "../components/FormModal";
|
||||
import { useForm } from "./useForm";
|
||||
import { FormItem } from "./useItems";
|
||||
|
||||
export type FormModalUseOptions = Partial<Omit<FormModalProps, "items">> & {
|
||||
items: FormItem[];
|
||||
};
|
||||
|
||||
export function useFormModal(options: FormModalUseOptions) {
|
||||
const { model, items, formProps } = useForm({ ...options, submit: undefined });
|
||||
const trigger = ref(options.trigger);
|
||||
const title = ref(options.title);
|
||||
const modalProps = ref(options.modalProps);
|
||||
const modalRef = ref<InstanceType<typeof AnFormModal> | null>(null);
|
||||
const submit = ref(options.submit);
|
||||
const formRef = computed(() => modalRef.value?.formRef);
|
||||
const open = (data: Recordable = {}) => modalRef.value?.open(data);
|
||||
|
||||
const component = () => {
|
||||
return (
|
||||
<AnFormModal
|
||||
ref={(el: any) => (modalRef.value = el)}
|
||||
title={title.value}
|
||||
trigger={trigger.value}
|
||||
modalProps={modalProps.value as any}
|
||||
model={model.value}
|
||||
items={items.value}
|
||||
formProps={formProps.value}
|
||||
submit={submit.value}
|
||||
></AnFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
model,
|
||||
items,
|
||||
formProps,
|
||||
component,
|
||||
modalRef,
|
||||
formRef,
|
||||
open,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { defaultsDeep, merge, omit } from 'lodash-es';
|
||||
import { IAnFormItem, IAnFormItemBase } from '../components/FormItem';
|
||||
import { SetterItem, setterMap } from '../components/FormSetter';
|
||||
import { Rule, useRules } from './useRules';
|
||||
|
||||
/**
|
||||
* 表单项数据
|
||||
*/
|
||||
export type FormItem = Omit<IAnFormItemBase, 'rules'> &
|
||||
SetterItem & {
|
||||
/**
|
||||
* 默认值
|
||||
* @example
|
||||
* ```ts
|
||||
* '1'
|
||||
* ```
|
||||
*/
|
||||
value?: any;
|
||||
|
||||
/**
|
||||
* 是否必填
|
||||
* @default
|
||||
* ```ts
|
||||
* false
|
||||
* ```
|
||||
*/
|
||||
required?: boolean;
|
||||
|
||||
/**
|
||||
* 校验规则
|
||||
* @example
|
||||
* ```ts
|
||||
* ['email']
|
||||
* ```
|
||||
*/
|
||||
rules?: Rule[];
|
||||
};
|
||||
|
||||
const ITEM: Partial<FormItem> = {
|
||||
setter: 'input',
|
||||
};
|
||||
|
||||
export function useItems(list: FormItem[], model: Recordable) {
|
||||
const items = ref<IAnFormItem[]>([]);
|
||||
|
||||
for (const item of list) {
|
||||
let target: any = defaultsDeep({}, ITEM);
|
||||
|
||||
if (!item.setter || typeof item.setter === 'string') {
|
||||
const setter = setterMap[item.setter ?? 'input'];
|
||||
if (setter) {
|
||||
defaultsDeep(target, { setterProps: setter.setterProps ?? {} });
|
||||
}
|
||||
}
|
||||
|
||||
target = merge(target, omit(item, ['required', 'rules', 'value']));
|
||||
|
||||
const rules = useRules(item);
|
||||
if (rules) {
|
||||
target.rules = rules;
|
||||
}
|
||||
|
||||
model[item.field] = model[item.field] ?? item.value;
|
||||
items.value.push(target);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { FieldRule } from "@arco-design/web-vue";
|
||||
import { has, isString } from "lodash-es";
|
||||
import { 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 data: IAnFormItemRule[] = [];
|
||||
const { required, rules } = item;
|
||||
|
||||
if (!has(item, "required") && !has(item, "rules")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export * from './components/Form';
|
||||
export * from './hooks/useForm';
|
||||
export * from './components/useFormContext';
|
||||
export * from './components/FormItem';
|
||||
export * from './components/useFormModel';
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { Cascader, CascaderInstance } from '@arco-design/web-vue';
|
||||
import { initOptions } from '../utils/initOptions';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type CascaderProps = CascaderInstance['$props'];
|
||||
|
||||
type CascaderSlots = 'label' | 'prefix' | 'arrowIcon' | 'loadingIcon' | 'searchIcon' | 'empty' | 'option';
|
||||
|
||||
export default defineSetter<CascaderProps, CascaderSlots>({
|
||||
setter: Cascader,
|
||||
setterProps: {
|
||||
placeholder: '请选择',
|
||||
allowClear: true,
|
||||
expandTrigger: 'hover',
|
||||
},
|
||||
onSetup: initOptions as any,
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { DatePicker, DatePickerInstance } from '@arco-design/web-vue';
|
||||
import { defineSetter } from './util';
|
||||
import { PickerProps } from '@arco-design/web-vue/es/date-picker/interface';
|
||||
|
||||
type DateProps = DatePickerInstance['$props'] & Partial<PickerProps>;
|
||||
|
||||
type DateSlots =
|
||||
| 'prefix'
|
||||
| 'suffixIcon'
|
||||
| 'iconNextDouble'
|
||||
| 'iconPrevDouble'
|
||||
| 'iconNext'
|
||||
| 'iconPrev'
|
||||
| 'cell'
|
||||
| 'extra';
|
||||
|
||||
export default defineSetter<DateProps, DateSlots>({
|
||||
setter: DatePicker,
|
||||
setterProps: {
|
||||
placeholder: '请选择',
|
||||
allowClear: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { RangePicker, RangePickerInstance } from '@arco-design/web-vue';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type RangeProps = RangePickerInstance['$props'];
|
||||
|
||||
type RangeSlots = "none";
|
||||
|
||||
export default defineSetter<RangeProps, RangeSlots>({
|
||||
setter: RangePicker,
|
||||
setterProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Input, InputInstance } from '@arco-design/web-vue';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type InputProps = InputInstance['$props'];
|
||||
|
||||
type InputSlots = 'prepend' | 'append' | 'suffix' | 'prefix';
|
||||
|
||||
export default defineSetter<InputProps, InputSlots>({
|
||||
setter: Input,
|
||||
setterProps: {
|
||||
placeholder: '请输入',
|
||||
allowClear: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { InputInstance, InputNumber, InputNumberInstance } from '@arco-design/web-vue';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type NumberProps = InputInstance['$props'] | InputNumberInstance['$props'];
|
||||
|
||||
type NumberSlots = 'minus' | 'plus' | 'append' | 'prepend' | 'suffix' | 'prefix';
|
||||
|
||||
export default defineSetter<NumberProps, NumberSlots>({
|
||||
setter: InputNumber,
|
||||
setterProps: {
|
||||
placeholder: '请输入',
|
||||
defaultValue: 0,
|
||||
allowClear: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { InputInstance, InputPassword, InputPasswordInstance } from '@arco-design/web-vue';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type PasswordProps = InputInstance['$props'] & InputPasswordInstance['$props'];
|
||||
|
||||
type PasswordSlots = 'none';
|
||||
|
||||
export default defineSetter<PasswordProps, PasswordSlots>({
|
||||
setter: InputPassword,
|
||||
setterProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { InputInstance, InputSearch, InputSearchInstance } from '@arco-design/web-vue';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type SearchProps = InputInstance['$props'] & InputSearchInstance['$props'];
|
||||
|
||||
type SearchSlots = "none";
|
||||
|
||||
export default defineSetter<SearchProps, SearchSlots>({
|
||||
setter: InputSearch,
|
||||
setterProps: {
|
||||
placeholder: '请输入',
|
||||
allowClear: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { Select, SelectInstance } from '@arco-design/web-vue';
|
||||
import { initOptions } from '../utils/initOptions';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type SelectProps = SelectInstance['$props'];
|
||||
|
||||
type SelectSlots =
|
||||
| 'trigger'
|
||||
| 'prefix'
|
||||
| 'searchIcon'
|
||||
| 'loadingIcon'
|
||||
| 'arrowIcon'
|
||||
| 'footer'
|
||||
| 'header'
|
||||
| 'label'
|
||||
| 'option'
|
||||
| 'empty';
|
||||
|
||||
export default defineSetter<SelectProps, SelectSlots>({
|
||||
setter: Select,
|
||||
onSetup: initOptions as any,
|
||||
setterProps: {
|
||||
placeholder: '请选择',
|
||||
allowClear: true,
|
||||
allowSearch: true,
|
||||
options: [],
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { Button } from '@arco-design/web-vue';
|
||||
import { FormContextKey } from '../components/useFormContext';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
export default defineSetter<{}, 'none'>({
|
||||
setter() {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
},
|
||||
setterProps: {},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { InputInstance, Textarea, TextareaInstance } from '@arco-design/web-vue';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type TextareaProps = InputInstance['$props'] & TextareaInstance['$props'];
|
||||
|
||||
type TextareaSlots = "none";
|
||||
|
||||
export default defineSetter<TextareaProps, TextareaSlots>({
|
||||
setter: Textarea,
|
||||
setterProps: {
|
||||
placeholder: '请输入',
|
||||
allowClear: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { TimePicker, TimePickerInstance } from '@arco-design/web-vue';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type TimeProps = TimePickerInstance['$props'];
|
||||
|
||||
type TimeSlots = 'prefix' | 'suffixIcon' | 'extra';
|
||||
|
||||
export default defineSetter<TimeProps, TimeSlots>({
|
||||
setter: TimePicker,
|
||||
setterProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { TreeSelect, TreeSelectInstance } from '@arco-design/web-vue';
|
||||
import { initOptions } from '../utils/initOptions';
|
||||
import { defineSetter } from './util';
|
||||
|
||||
type TreeSelectProps = TreeSelectInstance['$props'];
|
||||
|
||||
type TreeSelectSlots =
|
||||
| 'trigger'
|
||||
| 'prefix'
|
||||
| 'label'
|
||||
| 'header'
|
||||
| 'loader'
|
||||
| 'empty'
|
||||
| 'footer'
|
||||
| 'treeSlotExtra'
|
||||
| 'treeSlotTitle'
|
||||
| 'treeSlotIcon'
|
||||
| 'treeSlotSwitcherIcon';
|
||||
|
||||
export default defineSetter<TreeSelectProps, TreeSelectSlots>({
|
||||
setter: TreeSelect,
|
||||
onSetup: (arg: any) => initOptions(arg, 'data') as any,
|
||||
setterProps: {
|
||||
placeholder: '请选择',
|
||||
allowClear: true,
|
||||
allowSearch: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import cascader from './Cascader';
|
||||
import date from './Date';
|
||||
import dateRange from './DateRange';
|
||||
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';
|
||||
|
||||
export default {
|
||||
input,
|
||||
number,
|
||||
search,
|
||||
textarea,
|
||||
select,
|
||||
treeSelect,
|
||||
time,
|
||||
password,
|
||||
cascader,
|
||||
date,
|
||||
submit,
|
||||
dateRange,
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { Component } from 'vue';
|
||||
import { IAnFormItemBase, IAnFormItemSlot } from '../components/FormItem';
|
||||
|
||||
export interface ItemSetter<P extends object, S extends string> {
|
||||
/**
|
||||
* 输入组件
|
||||
*/
|
||||
setter: Component;
|
||||
|
||||
/**
|
||||
* 输入组件参数
|
||||
*/
|
||||
setterProps?: P;
|
||||
|
||||
/**
|
||||
* 空间插槽
|
||||
*/
|
||||
setterSlots?: {
|
||||
/**
|
||||
* 控件插槽
|
||||
* @example
|
||||
* ```tsx
|
||||
* (props) => {
|
||||
* return <span>{props.item.label}</span>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
[key in S]?: IAnFormItemSlot;
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
onSetup?: (args: { model: Recordable; item: IAnFormItemBase; items: IAnFormItemBase[] }) => void;
|
||||
}
|
||||
|
||||
export function defineSetter<P extends object, S extends string>(setter: ItemSetter<P, S>) {
|
||||
return setter;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export function strOrFnRender(fn: any, options: any) {
|
||||
if (typeof fn === "string") {
|
||||
return () => fn;
|
||||
}
|
||||
if (typeof fn === "function") {
|
||||
return fn(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
import { AnForm, AnFormInstance, IAnForm } from '@/components/AnForm';
|
||||
import AniEmpty from '@/components/empty/AniEmpty.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 { PropType, defineComponent, ref } from 'vue';
|
||||
import { PluginContainer } from '../hooks/useTablePlugin';
|
||||
|
||||
type DataFn = (filter: { page: number; size: number; [key: string]: any }) => any | Promise<any>;
|
||||
|
||||
/**
|
||||
* 表格组件
|
||||
*/
|
||||
export const AnTable = defineComponent({
|
||||
name: 'AnTable',
|
||||
props: {
|
||||
/**
|
||||
* 表格数据
|
||||
* @description 数组或者函数
|
||||
*/
|
||||
data: {
|
||||
type: [Array, Function] as PropType<BaseData[] | DataFn>,
|
||||
},
|
||||
/**
|
||||
* 表格列
|
||||
*/
|
||||
columns: {
|
||||
type: Array as PropType<BaseColumn[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* 分页配置
|
||||
*/
|
||||
paging: {
|
||||
type: Object as PropType<PaginationProps & { hide?: boolean }>,
|
||||
},
|
||||
/**
|
||||
* 搜索表单
|
||||
*/
|
||||
search: {
|
||||
type: Object as PropType<IAnForm>,
|
||||
},
|
||||
/**
|
||||
* 新建弹窗
|
||||
*/
|
||||
create: {
|
||||
type: Object as PropType<FormModalProps>,
|
||||
},
|
||||
/**
|
||||
* 修改弹窗
|
||||
*/
|
||||
modify: {
|
||||
type: Object as PropType<FormModalProps>,
|
||||
},
|
||||
/**
|
||||
* 传递给 Table 组件的属性
|
||||
*/
|
||||
tableProps: {
|
||||
type: Object as PropType<InstanceType<typeof BaseTable>['$props']>,
|
||||
},
|
||||
/**
|
||||
* 插件列表
|
||||
*/
|
||||
pluginer: {
|
||||
type: Object as PropType<PluginContainer>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const loading = ref(false);
|
||||
const tableRef = ref<InstanceType<typeof BaseTable>>();
|
||||
const renderData = ref<BaseData[]>([]);
|
||||
const searchRef = ref<AnFormInstance | null>(null);
|
||||
|
||||
const useTablePaging = () => {
|
||||
const getPaging = () => {
|
||||
return {
|
||||
page: props.paging?.current ?? 1,
|
||||
size: props.paging?.pageSize ?? 10,
|
||||
};
|
||||
};
|
||||
|
||||
const setPaging = (paging: PaginationProps) => {
|
||||
if (props.paging) {
|
||||
merge(props.paging, paging);
|
||||
}
|
||||
};
|
||||
|
||||
const resetPaging = () => {
|
||||
setPaging({ current: 1, pageSize: 10 });
|
||||
};
|
||||
|
||||
return {
|
||||
getPaging,
|
||||
setPaging,
|
||||
resetPaging,
|
||||
};
|
||||
};
|
||||
|
||||
const { getPaging, setPaging, resetPaging } = useTablePaging();
|
||||
|
||||
const loadData = async () => {
|
||||
if (await searchRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paging = getPaging();
|
||||
const search = searchRef.value?.getModel() ?? {};
|
||||
|
||||
if (isArray(props.data)) {
|
||||
// todo
|
||||
}
|
||||
|
||||
if (isFunction(props.data)) {
|
||||
try {
|
||||
loading.value = true;
|
||||
let params = { ...search, ...paging };
|
||||
params = props.pluginer?.callBeforeSearchHook(params) ?? params;
|
||||
const resData = await props.data(params);
|
||||
const { data = [], total = 0 } = resData?.data || {};
|
||||
renderData.value = data;
|
||||
setPaging({ total });
|
||||
} catch (e) {
|
||||
// todo
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
setPaging({ current: 1, pageSize: 10 });
|
||||
return loadData();
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
return loadData();
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (Array.isArray(props.data)) {
|
||||
renderData.value = props.data;
|
||||
resetPaging();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
props.pluginer?.callPageChangeHook(page);
|
||||
setPaging({ current: page });
|
||||
loadData();
|
||||
};
|
||||
|
||||
const onPageSizeChange = (size: number) => {
|
||||
props.pluginer?.callSizeChangeHook(size);
|
||||
setPaging({ current: 1, pageSize: size });
|
||||
loadData();
|
||||
};
|
||||
|
||||
const state = {
|
||||
loading,
|
||||
tableRef,
|
||||
searchRef,
|
||||
renderData,
|
||||
loadData,
|
||||
reload,
|
||||
refresh,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
props,
|
||||
};
|
||||
|
||||
props.pluginer?.callSetupHook(state);
|
||||
|
||||
provide('ref:table', { ...state, ...props });
|
||||
|
||||
return state;
|
||||
},
|
||||
render() {
|
||||
(this.columns as any).instance = this;
|
||||
return (
|
||||
<div class="table w-full">
|
||||
<div class={`mb-3 flex toolbar justify-between`}>
|
||||
{this.pluginer?.actions && (
|
||||
<div class={`flex-1 flex gap-2 items-center`}>
|
||||
{this.pluginer.actions.map(Action => (
|
||||
<Action />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{this.search && (
|
||||
<AnForm
|
||||
ref="searchRef"
|
||||
v-model:model={this.search.model}
|
||||
items={this.search.items}
|
||||
formProps={this.search.formProps}
|
||||
>
|
||||
{{
|
||||
submit: () => (
|
||||
<Button type="primary" loading={this.loading} onClick={this.reload}>
|
||||
{{
|
||||
default: () => '查询',
|
||||
icon: () => <i class="icon-park-outline-search"></i>,
|
||||
}}
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
</AnForm>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex gap-2 ml-2">
|
||||
<div class="flex gap-1">{this.pluginer?.widgets && this.pluginer.widgets?.map(Widget => <Widget />)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseTable
|
||||
row-key="id"
|
||||
bordered={false}
|
||||
{...this.$attrs}
|
||||
{...this.tableProps}
|
||||
ref="tableRef"
|
||||
loading={this.loading}
|
||||
pagination={this.paging?.hide ? false : this.paging}
|
||||
data={this.renderData}
|
||||
columns={this.columns}
|
||||
onPageChange={this.onPageChange}
|
||||
onPageSizeChange={this.onPageSizeChange}
|
||||
>
|
||||
{{
|
||||
empty: () => <AniEmpty />,
|
||||
...this.$slots,
|
||||
}}
|
||||
</BaseTable>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 表格组件实例
|
||||
*/
|
||||
export type TableInstance = InstanceType<typeof AnTable>;
|
||||
|
||||
/**
|
||||
* 表格组件参数
|
||||
*/
|
||||
export type TableProps = TableInstance['$props'];
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { cloneDeep, merge } from "lodash-es";
|
||||
import { IAnFormItem } from "../../AnForm/components/FormItem";
|
||||
import { FormModalProps } from "../../AnForm/components/FormModal";
|
||||
import { FormModalUseOptions } from "../../AnForm/hooks/useFormModal";
|
||||
import { ExtendFormItem } from "./useSearchForm";
|
||||
|
||||
export type ModifyForm = Omit<FormModalUseOptions, "items"> & {
|
||||
/**
|
||||
* 是否继承新建弹窗
|
||||
*/
|
||||
extend?: boolean;
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
items?: ExtendFormItem[];
|
||||
};
|
||||
|
||||
export function useModifyForm(form: ModifyForm, create: FormModalProps) {
|
||||
const { extend, items, ...rest } = form;
|
||||
let result = {};
|
||||
if (extend) {
|
||||
cloneDeep(create ?? {});
|
||||
const createItems = create.items;
|
||||
const modifyItems = form.items;
|
||||
if (modifyItems && createItems) {
|
||||
for (let i = 0; i < modifyItems.length; i++) {
|
||||
if (modifyItems[i].extend) {
|
||||
modifyItems[i] = merge({}, createItems[i], modifyItems[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { defaultsDeep, isArray, merge } from 'lodash-es';
|
||||
import { FormUseOptions } from '../../AnForm';
|
||||
import { IAnFormItem } from '../../AnForm/components/FormItem';
|
||||
import { FormItem, useItems } from '../../AnForm/hooks/useItems';
|
||||
|
||||
export type ExtendFormItem = Partial<
|
||||
FormItem & {
|
||||
/**
|
||||
* 从新建弹窗继承表单项
|
||||
* @example 'name'
|
||||
*/
|
||||
extend: string;
|
||||
}
|
||||
>;
|
||||
|
||||
type SearchFormItem = ExtendFormItem & {
|
||||
/**
|
||||
* 是否点击图标后进行搜索
|
||||
* @default false
|
||||
*/
|
||||
searchable?: boolean;
|
||||
/**
|
||||
* 是否回车后进行查询
|
||||
* @default false
|
||||
*/
|
||||
enterable?: boolean;
|
||||
};
|
||||
|
||||
export type SearchFormObject = Omit<FormUseOptions, 'items' | 'submit'> & {
|
||||
/**
|
||||
* 搜索表单项
|
||||
*/
|
||||
items?: SearchFormItem[];
|
||||
/**
|
||||
* 是否隐藏查询按钮
|
||||
* @default false
|
||||
*/
|
||||
hideSearch?: boolean;
|
||||
};
|
||||
|
||||
export type SearchForm = SearchFormObject | SearchFormItem[];
|
||||
|
||||
export function useSearchForm(search?: SearchForm, extendItems: IAnFormItem[] = []) {
|
||||
if (!search) {
|
||||
return ref();
|
||||
}
|
||||
|
||||
if (isArray(search)) {
|
||||
search = { items: 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 props = ref({
|
||||
items: [] as IAnFormItem[],
|
||||
model: _model ?? {},
|
||||
formProps: defaultsDeep({}, _formProps, { layout: 'inline' }),
|
||||
});
|
||||
|
||||
const defualts: Partial<IAnFormItem> = {
|
||||
setter: 'input',
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
setterProps: {},
|
||||
};
|
||||
|
||||
const items: any[] = [];
|
||||
for (const _item of _items) {
|
||||
const { searchable, enterable, field, extend, ...itemRest } = _item;
|
||||
if ((field || extend) === 'submit' && hideSearch) {
|
||||
continue;
|
||||
}
|
||||
let item: IAnFormItem = defaultsDeep({}, itemRest, defualts);
|
||||
if (extend) {
|
||||
const extendItem = extendMap[extend];
|
||||
if (extendItem) {
|
||||
item = merge({}, extendItem, itemRest);
|
||||
}
|
||||
}
|
||||
if (searchable) {
|
||||
(item as any).nodeProps.onSearch = () => null;
|
||||
}
|
||||
if (enterable) {
|
||||
(item as any).nodeProps.onPressEnter = () => null;
|
||||
}
|
||||
if (item.setterProps) {
|
||||
(item.setterProps as any).placeholder = (item.setterProps as any).placeholder ?? item.label;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
props.value.items = useItems(items, props.value.model).value;
|
||||
|
||||
return props;
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import { FormModalUseOptions, useFormModal } from '../../AnForm/hooks/useFormModal';
|
||||
import { AnTable, TableProps } from '../components/Table';
|
||||
import { ModifyForm } from './useModiyForm';
|
||||
import { SearchForm, useSearchForm } from './useSearchForm';
|
||||
import { TableColumn, useTableColumns } from './useTableColumn';
|
||||
import { AnTablePlugin, PluginContainer } from './useTablePlugin';
|
||||
|
||||
export interface TableUseOptions extends Pick<TableProps, 'data' | 'tableProps' | 'paging'> {
|
||||
/**
|
||||
* 唯一ID
|
||||
* @example
|
||||
* ```ts
|
||||
* 'UserTable'
|
||||
* ```
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* 插件列表
|
||||
* @example
|
||||
* ```ts
|
||||
* [useRefresh()]
|
||||
* ```
|
||||
*/
|
||||
plugins?: AnTablePlugin[];
|
||||
/**
|
||||
* 表格列
|
||||
* @example
|
||||
* ```ts
|
||||
* [{
|
||||
* dataIndex: 'title',
|
||||
* title: '标题'
|
||||
* }]
|
||||
* ```
|
||||
*/
|
||||
columns?: TableColumn[];
|
||||
/**
|
||||
* 搜索表单
|
||||
* @example
|
||||
* ```ts
|
||||
* [{
|
||||
* field: 'name',
|
||||
* label: '用户名称',
|
||||
* setter: 'input'
|
||||
* }]
|
||||
* ```
|
||||
*/
|
||||
search?: SearchForm;
|
||||
/**
|
||||
* 新建弹窗
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* title: '添加用户',
|
||||
* items: [],
|
||||
* submit: (model) => {}
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
create?: FormModalUseOptions;
|
||||
/**
|
||||
* 修改弹窗
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* extend: true, // 基于新建弹窗扩展
|
||||
* title: '修改用户',
|
||||
* submit: (model) => {}
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
modify?: ModifyForm;
|
||||
}
|
||||
|
||||
export function useTable(options: TableUseOptions) {
|
||||
const pluginer = new PluginContainer(options.plugins ?? []);
|
||||
|
||||
options = pluginer.callOptionsHook(options);
|
||||
|
||||
const { columns } = useTableColumns(options.columns ?? []);
|
||||
const data = ref(options.data);
|
||||
const pagination = ref({ hide: false, showTotal: true, showPageSize: true, ...(options.paging ?? {}) });
|
||||
const tableProps = ref(options.tableProps ?? {});
|
||||
const tableRef = ref<InstanceType<typeof AnTable> | null>(null);
|
||||
const searchProps = useSearchForm(options.search);
|
||||
// const create = options.create && useFormModal(options.create);
|
||||
|
||||
const AnTabler = () => (
|
||||
<AnTable
|
||||
ref={(el: any) => (tableRef.value = el)}
|
||||
columns={columns.value}
|
||||
data={data.value}
|
||||
paging={pagination.value}
|
||||
tableProps={tableProps.value}
|
||||
search={searchProps.value}
|
||||
pluginer={pluginer}
|
||||
></AnTable>
|
||||
);
|
||||
|
||||
return {
|
||||
component: AnTabler,
|
||||
columns,
|
||||
tableRef,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import { Divider, Link, TableColumnData } from '@arco-design/web-vue';
|
||||
|
||||
interface TableBaseColumn {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type?: undefined;
|
||||
}
|
||||
|
||||
interface TableIndexColumn {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type: 'index';
|
||||
}
|
||||
|
||||
interface TableColumnButton {
|
||||
/**
|
||||
* 特殊类型
|
||||
* @example
|
||||
* ```ts
|
||||
* 'delete'
|
||||
* ```
|
||||
*/
|
||||
type?: 'modify' | 'delete';
|
||||
/**
|
||||
* 按钮文本
|
||||
* @example
|
||||
* ```ts
|
||||
* '修改'
|
||||
* ```
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* 按钮参数
|
||||
* @see ALink
|
||||
*/
|
||||
buttonProps?: Recordable;
|
||||
/**
|
||||
* 是否可见
|
||||
* @example
|
||||
* ```ts
|
||||
* (props) => props.record.status === 1
|
||||
* ```
|
||||
*/
|
||||
visible?: (args: Recordable) => boolean;
|
||||
/**
|
||||
* 是否禁用
|
||||
* @example
|
||||
* ```ts
|
||||
* (props) => props.record.status === 1
|
||||
* ```
|
||||
*/
|
||||
disable?: (args: Recordable) => boolean;
|
||||
/**
|
||||
* 处理函数
|
||||
* @example
|
||||
* ```ts
|
||||
* (props) => api.user.rmUser(props.record.id)
|
||||
* ```
|
||||
*/
|
||||
onClick?: (props: any) => void;
|
||||
}
|
||||
|
||||
interface TableButtonColumn {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type: 'button';
|
||||
/**
|
||||
* 按钮列表
|
||||
* @example
|
||||
* ```ts
|
||||
* [{ text: '删除', onClick: (args) => api.user.rmUser(args.record.id) }]
|
||||
* ```
|
||||
*/
|
||||
buttons: TableColumnButton[];
|
||||
}
|
||||
|
||||
interface TableDropdownColumn {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type: 'dropdown';
|
||||
/**
|
||||
* 下拉列表
|
||||
*/
|
||||
dropdowns: any[];
|
||||
}
|
||||
|
||||
export type TableColumn = TableColumnData &
|
||||
(TableIndexColumn | TableBaseColumn | TableButtonColumn | TableDropdownColumn) & {
|
||||
/**
|
||||
* 是否可配置
|
||||
* @example
|
||||
* ```ts
|
||||
* true
|
||||
* ```
|
||||
*/
|
||||
configable?: boolean;
|
||||
};
|
||||
|
||||
export function useTableColumns(data: TableColumn[]) {
|
||||
const columns = ref<TableColumnData[]>([]);
|
||||
|
||||
// for (let column of data) {
|
||||
// if (column.type === "index") {
|
||||
// column = useTableIndexColumn(column);
|
||||
// }
|
||||
|
||||
for (let column of data) {
|
||||
if (column.type === 'button') {
|
||||
column = useTableButtonColumn(column);
|
||||
}
|
||||
columns.value.push(column);
|
||||
}
|
||||
|
||||
// if (column.type === "dropdown") {
|
||||
// column = useTableDropdownColumn(column);
|
||||
// }
|
||||
|
||||
// columns.push({ ...config.columnBase, ...column });
|
||||
// }
|
||||
|
||||
return {
|
||||
columns,
|
||||
};
|
||||
}
|
||||
|
||||
function useTableIndexColumn() {}
|
||||
|
||||
function useTableButtonColumn(column: TableButtonColumn & TableColumnData) {
|
||||
const { type, buttons } = column;
|
||||
const items: TableColumnButton[] = [];
|
||||
for (const button of buttons) {
|
||||
items.push(button);
|
||||
}
|
||||
column.render = props => {
|
||||
return items.map((item, index) => {
|
||||
if (item.visible && !item.visible(props)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{index !== 0 && <Divider direction="vertical" margin={4} />}
|
||||
<Link {...item.buttonProps} disabled={item.disable?.(props)} onClick={() => item.onClick?.(props)}>
|
||||
{item.text}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
return column;
|
||||
}
|
||||
|
||||
function useTableDropdownColumn() {}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { TableUseOptions } from './useTable';
|
||||
|
||||
export interface AnTablePlugin {
|
||||
/**
|
||||
* 插件ID(唯一)
|
||||
* @example
|
||||
* ```ts
|
||||
* 'Plugin:Refresh'
|
||||
* ```
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* 提供给其他插件使用的变量
|
||||
* @example
|
||||
* ```ts
|
||||
* { isOk: true }
|
||||
* ```
|
||||
*/
|
||||
provide?: Recordable;
|
||||
/**
|
||||
* 组件钩子
|
||||
*/
|
||||
onSetup?: (context: any) => void;
|
||||
/**
|
||||
* 钩子
|
||||
*/
|
||||
options?: (options: TableUseOptions) => TableUseOptions | null | undefined | void;
|
||||
/**
|
||||
* 添加部件栏组件
|
||||
*/
|
||||
widget?: () => any;
|
||||
/**
|
||||
* 添加操作栏组件
|
||||
*/
|
||||
action?: () => any;
|
||||
/**
|
||||
* 搜索前处理
|
||||
*
|
||||
*/
|
||||
onBeforeSearch?: (args: { page: number; size: number; [key: string]: any }) => Recordable | null | undefined | void;
|
||||
onPageChange?: (page: number) => void;
|
||||
onSizeChange?: (size: number) => void;
|
||||
}
|
||||
|
||||
export class PluginContainer {
|
||||
actions: any[] = [];
|
||||
widgets: any[] = [];
|
||||
|
||||
constructor(private plugins: AnTablePlugin[]) {
|
||||
for (const plugin of plugins) {
|
||||
const action = plugin.action?.();
|
||||
const widget = plugin.widget?.();
|
||||
if (action) {
|
||||
this.actions.push(action);
|
||||
}
|
||||
if (widget) {
|
||||
this.widgets.push(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callSetupHook(context: any) {
|
||||
for (const plugin of this.plugins) {
|
||||
plugin.onSetup?.(context);
|
||||
}
|
||||
}
|
||||
|
||||
callOptionsHook(options: any) {
|
||||
for (const plugin of this.plugins) {
|
||||
options = plugin.options?.(options) ?? options;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
callActionHook(options: any) {
|
||||
for (const plugin of this.plugins) {
|
||||
options = plugin.options?.(options) ?? options;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
callWidgetHook(options: any) {
|
||||
for (const plugin of this.plugins) {
|
||||
options = plugin.options?.(options) ?? options;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
callBeforeSearchHook(options: any) {
|
||||
for (const plugin of this.plugins) {
|
||||
options = plugin.onBeforeSearch?.(options) ?? options;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
callPageChangeHook(page: number) {
|
||||
for (const plugin of this.plugins) {
|
||||
plugin.onPageChange?.(page);
|
||||
}
|
||||
}
|
||||
|
||||
callSizeChangeHook(page: number) {
|
||||
for (const plugin of this.plugins) {
|
||||
plugin.onPageChange?.(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './hooks/useTable';
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
import { Button, Checkbox, Divider, InputNumber, Popover, Scrollbar, Tag } from '@arco-design/web-vue';
|
||||
import { PropType } from 'vue';
|
||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
||||
|
||||
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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 插件:表格列配置
|
||||
* @description 配置ID将缓存结果在本地
|
||||
*/
|
||||
export function useColumnConfig(): AnTablePlugin {
|
||||
let context: any;
|
||||
return {
|
||||
id: "columnconfig",
|
||||
onSetup(args) {
|
||||
context = args;
|
||||
},
|
||||
widget() {
|
||||
return () => <TableColumnConfig columns={context.props.columns} />;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { Button } from '@arco-design/web-vue';
|
||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
||||
|
||||
/**
|
||||
* 插件:添加刷新按钮
|
||||
* @description 位于搜索栏附近
|
||||
*/
|
||||
export function useRefresh(): AnTablePlugin {
|
||||
let context: any = {};
|
||||
|
||||
return {
|
||||
id: 'refresh',
|
||||
onSetup(ctx) {
|
||||
context = ctx;
|
||||
},
|
||||
widget() {
|
||||
return () => {
|
||||
const { loading, refresh } = context;
|
||||
return (
|
||||
<Button disabled={loading.value} onClick={refresh}>
|
||||
{{
|
||||
icon: () => <span class="icon-park-outline-redo"></span>,
|
||||
}}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { cloneDeep, defaultsDeep, merge } from 'lodash-es';
|
||||
import { TableUseOptions } from '../hooks/useTable';
|
||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
||||
|
||||
// declare module '@/components/AnTable/hooks/useTable' {
|
||||
// interface TableUseOptions {
|
||||
// todo?: string;
|
||||
// }
|
||||
// }
|
||||
|
||||
const defaults: TableUseOptions = {
|
||||
tableProps: {
|
||||
rowSelection: {
|
||||
showCheckedAll: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function useSelection<T extends any>({ key = 'id', mode = 'key' } = {}): AnTablePlugin {
|
||||
const selected = ref<T[]>([]);
|
||||
|
||||
return {
|
||||
id: 'selection',
|
||||
provide: {
|
||||
selected,
|
||||
},
|
||||
options(options) {
|
||||
const opts: TableUseOptions = defaultsDeep({}, defaults);
|
||||
|
||||
if (!opts.tableProps!.rowKey) {
|
||||
opts.tableProps!.rowKey = key;
|
||||
}
|
||||
|
||||
if (mode === 'key') {
|
||||
opts.tableProps!.onSelectionChange = rowkeys => {
|
||||
selected.value = rowkeys as any[];
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'row') {
|
||||
opts.tableProps!.onSelect = (rowkeys, rowkey, record) => {
|
||||
const index = selected.value.findIndex((i: any) => i[key] == record[key]);
|
||||
if (index > -1) {
|
||||
selected.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
opts.tableProps!.onSelectAll = checked => {
|
||||
if (checked) {
|
||||
selected.value = cloneDeep([]);
|
||||
} else {
|
||||
selected.value = [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return merge(options, opts);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { nodeMap, NodeMap, NodeType } from "../nodes";
|
||||
|
||||
type NodeUnion = {
|
||||
[key in NodeType]: Partial<
|
||||
NodeMap[key] & {
|
||||
/**
|
||||
* 组件类型
|
||||
*/
|
||||
type: key;
|
||||
}
|
||||
>;
|
||||
}[NodeType];
|
||||
|
||||
export { nodeMap };
|
||||
export type { NodeMap, NodeType, NodeUnion };
|
||||
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
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 部分属性会不可用,如field、label、required、rules、disabled等
|
||||
*/
|
||||
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 同FormItem组件的label属性
|
||||
*/
|
||||
label?: string | ((args: FormItemFnArg) => any);
|
||||
|
||||
/**
|
||||
* 帮助提示
|
||||
* @description 同FormItem组件的help插槽
|
||||
*/
|
||||
help?: string | ((args: FormItemFnArg) => any);
|
||||
|
||||
/**
|
||||
* 额外内容
|
||||
* @description 同FormItem组件的extra插槽
|
||||
*/
|
||||
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>;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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>;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
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>;
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
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>;
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
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>;
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
export const config = {
|
||||
item: {
|
||||
defaults: {
|
||||
type: "input",
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 获取API错误信息
|
||||
*/
|
||||
|
|
@ -79,10 +84,12 @@ export const config = {
|
|||
export function initOptions({ item, model }: any, key = "options") {
|
||||
if (Array.isArray(item.options)) {
|
||||
item.nodeProps[key] = item.options;
|
||||
return;
|
||||
}
|
||||
if (item.options && typeof item.options === "object") {
|
||||
const { value, source } = item.options;
|
||||
item._updateOptions = async () => {};
|
||||
return;
|
||||
}
|
||||
if (typeof item.options === "function") {
|
||||
const loadData = item.options;
|
||||
|
|
|
|||
|
|
@ -177,4 +177,4 @@ type FormItemBase = {
|
|||
extra?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
|
||||
};
|
||||
|
||||
export type IFormItem = FormItemBase & NodeUnion;
|
||||
export type IFormItem = FormItemBase & NodeUnion;
|
||||
|
|
@ -230,4 +230,4 @@ export type NodeUnion = {
|
|||
*/
|
||||
nodeProps?: NodeMap[key]["nodeProps"];
|
||||
};
|
||||
}[NodeType];
|
||||
}[NodeType];
|
||||
|
|
@ -43,4 +43,4 @@ export const RuleMap = defineRuleMap({
|
|||
match: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/,
|
||||
message: "至少包含大写字母、小写字母、数字和特殊字符",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { Form as BaseForm, FormInstance as BaseFormInstance, Message } from "@arco-design/web-vue";
|
||||
import { assign, cloneDeep, defaultsDeep } from "lodash-es";
|
||||
import { PropType } from "vue";
|
||||
import { config } from "./form-config";
|
||||
import { FormItem, IFormItem } from "./form-item";
|
||||
import { NodeType, nodeMap } from "./form-node";
|
||||
import { config } from "./form-config";
|
||||
|
||||
type SubmitFn = (arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>;
|
||||
|
||||
|
|
@ -117,4 +117,4 @@ export type FormInstance = InstanceType<typeof Form>;
|
|||
|
||||
export type FormProps = FormInstance["$props"];
|
||||
|
||||
export type FormDefinedProps = Pick<FormProps, "model" | "items" | "submit" | "formProps">;
|
||||
export type FormDefinedProps = Pick<FormProps, "model" | "items" | "submit" | "formProps">;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
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 同FormItem组件的label属性
|
||||
*/
|
||||
label?: string | IAnFormItemElemFn;
|
||||
|
||||
/**
|
||||
* 帮助提示
|
||||
* @description 同FormItem组件的help插槽
|
||||
*/
|
||||
help?: string | IAnFormItemElemFn;
|
||||
|
||||
/**
|
||||
* 额外内容
|
||||
* @description 同FormItem组件的extra插槽
|
||||
*/
|
||||
extra?: string | IAnFormItemElemFn;
|
||||
|
||||
/**
|
||||
* 是否必填
|
||||
* @description 默认值为false
|
||||
*/
|
||||
required?: boolean;
|
||||
|
||||
/**
|
||||
* 校验规则
|
||||
* @description 支持字符串(内置)、对象形式
|
||||
*/
|
||||
rules?: Rule[];
|
||||
|
||||
/**
|
||||
* 传递给`FormItem`组件的参数
|
||||
* @description 部分属性会不可用,如field、label、required、rules、disabled等
|
||||
*/
|
||||
itemProps?: Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">;
|
||||
|
||||
/**
|
||||
* 是否可见
|
||||
* @description 动态控制表单项是否可见
|
||||
*/
|
||||
visible?: IAnFormItemBoolFn;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
* @description 动态控制表单项是否禁用
|
||||
*/
|
||||
disable?: IAnFormItemBoolFn;
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单项
|
||||
*/
|
||||
export type FormItem = BaseFormItem & NodeUnion;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
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,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
render: () => {
|
||||
return "1";
|
||||
},
|
||||
nodeProps: {},
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { DatePicker, DatePickerInstance } from "@arco-design/web-vue";
|
||||
|
||||
type Props = DatePickerInstance["$props"];
|
||||
|
||||
export default {
|
||||
render: DatePicker,
|
||||
nodeProps: {
|
||||
placeholder: "请选择",
|
||||
allowClear: true,
|
||||
} as Props,
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { RangePicker, RangePickerInstance } from "@arco-design/web-vue";
|
||||
|
||||
type Props = RangePickerInstance["$props"];
|
||||
|
||||
export default {
|
||||
render: RangePicker,
|
||||
nodeProps: {
|
||||
allowClear: true,
|
||||
} as Props,
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Input, InputInstance } from "@arco-design/web-vue";
|
||||
|
||||
type Props = InputInstance["$props"];
|
||||
|
||||
export default {
|
||||
render: Input,
|
||||
nodeProps: {
|
||||
placeholder: "请输入",
|
||||
allowClear: true,
|
||||
} as Props,
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { InputInstance, InputPassword, InputPasswordInstance } from "@arco-design/web-vue";
|
||||
|
||||
type Props = InputInstance["$props"] & InputPasswordInstance["$props"];
|
||||
|
||||
export default {
|
||||
render: InputPassword,
|
||||
nodeProps: {
|
||||
placeholder: "请输入",
|
||||
} as Props,
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
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: [],
|
||||
// },
|
||||
// });
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
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: {},
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { TimePicker, TimePickerInstance } from "@arco-design/web-vue";
|
||||
|
||||
type Props = TimePickerInstance["$props"];
|
||||
|
||||
export default {
|
||||
render: TimePicker,
|
||||
nodeProps: {
|
||||
allowClear: true,
|
||||
} as Props,
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
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];
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { FormInstance } from "@arco-design/web-vue";
|
||||
import { IFormItem } from "./form-item";
|
||||
import { merge } from "lodash-es";
|
||||
import { IFormItem } from "./form-item";
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
|
|
@ -56,4 +56,4 @@ export const useForm = (options: Options) => {
|
|||
}
|
||||
|
||||
return reactive({ ...options, model, items }) as any;
|
||||
};
|
||||
};
|
||||
|
|
@ -31,4 +31,4 @@ export function setModel(model: any, data: Record<string, any>) {
|
|||
model[key] = data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export function strOrFnRender(fn: any, options: any) {
|
||||
if (typeof fn === "string") {
|
||||
return () => fn;
|
||||
}
|
||||
if (typeof fn === "function") {
|
||||
return fn(options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import AniEmpty from "@/components/empty/AniEmpty.vue";
|
||||
import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
|
||||
import { merge } from "lodash-es";
|
||||
import { PropType, computed, defineComponent, reactive, ref } from "vue";
|
||||
import AniEmpty from "../empty/AniEmpty.vue";
|
||||
import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form";
|
||||
import { config } from "./table.config";
|
||||
|
||||
|
|
@ -210,4 +210,4 @@ export type TableInstance = InstanceType<typeof Table>;
|
|||
/**
|
||||
* 表格组件参数
|
||||
*/
|
||||
export type TableProps = TableInstance["$props"];
|
||||
export type TableProps = TableInstance["$props"];
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export function useVisible(initial = false) {
|
||||
const visible = ref(initial);
|
||||
const show = () => (visible.value = true);
|
||||
const hide = () => (visible.value = false);
|
||||
const toggle = () => (visible.value = !visible.value);
|
||||
|
||||
return {
|
||||
visible,
|
||||
show,
|
||||
hide,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
</template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="menuStore.cacheAppNames">
|
||||
<component :is="Component"></component>
|
||||
<component v-if="!appStore.pageLoding" :is="Component"></component>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-spin>
|
||||
|
|
@ -73,11 +73,11 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from "@/store";
|
||||
import { useMenuStore } from "@/store/menu";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { IconSync } from "@arco-design/web-vue/es/icon";
|
||||
import Menu from "./components/menu.vue";
|
||||
import userDropdown from "./components/userDropdown.vue";
|
||||
import { useMenuStore } from "@/store/menu";
|
||||
|
||||
defineOptions({ name: "LayoutPage" });
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<a-popover v-model:popup-visible="visible" position="br" trigger="click">
|
||||
<a-button class="float-right">设置</a-button>
|
||||
<template #content>
|
||||
<div class="mb-1 leading-none border-b border-gray-100 pb-3">设置表格列</div>
|
||||
<a-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">
|
||||
<li
|
||||
v-for="(item, index) in items"
|
||||
:key="item.dataIndex"
|
||||
class="group flex items-center justify-between py-2 pr-8 select-none"
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<a-checkbox v-model="item.enable" :disabled="!item.editable" size="large" @change="onItemChange">
|
||||
{{ item.dataIndex }}
|
||||
</a-checkbox>
|
||||
<span class="hidden group-hover:inline-block ml-4">
|
||||
<i v-show="!item.editable" class="icon-park-outline-drag cursor-move"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<a-checkbox v-model="item.autoWidth" :disabled="!item.editable">
|
||||
<template #checkbox="{ checked }">
|
||||
<a-tag :checked="checked" :checkable="item.editable" color="blue">自适应</a-tag>
|
||||
</template>
|
||||
</a-checkbox>
|
||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
||||
<a-input-number
|
||||
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>
|
||||
</a-scrollbar>
|
||||
<div class="mt-4 flex gap-2 items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a-checkbox :indeterminate="indeterminate" v-model="checkAll" @change="onCheckAllChange"> 全选 </a-checkbox>
|
||||
<span class="text-xs text-gray-400 ml-1">
|
||||
({{ items.filter(i => i.enable).length }}/{{ items.length }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<a-button @click="onReset">重置</a-button>
|
||||
<a-button type="primary" @click="onConfirm">确定</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
interface Item {
|
||||
dataIndex: string;
|
||||
enable: boolean;
|
||||
autoWidth: boolean;
|
||||
width: number;
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
items.value.push({
|
||||
dataIndex: '顺序',
|
||||
enable: true,
|
||||
autoWidth: false,
|
||||
width: 80,
|
||||
editable: false,
|
||||
});
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
items.value.push({
|
||||
dataIndex: `测试${i}`,
|
||||
enable: true,
|
||||
autoWidth: false,
|
||||
width: 80,
|
||||
editable: true,
|
||||
});
|
||||
}
|
||||
items.value.push({
|
||||
dataIndex: '操作',
|
||||
enable: true,
|
||||
autoWidth: false,
|
||||
width: 80,
|
||||
editable: false,
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -1,17 +1,317 @@
|
|||
<template>
|
||||
<div class="m-4">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<i class="icon-park-outline-config mr-2"></i>
|
||||
系统参数
|
||||
</div>
|
||||
</template>
|
||||
</a-card>
|
||||
<div class="m-4 bg-white p-4">
|
||||
<user-table></user-table>
|
||||
<div>{{ formatModel(emodel) }}</div>
|
||||
<UpForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx"></script>
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { formatModel, useForm } from '@/components/AnForm';
|
||||
import { useTable } from '@/components/AnTable';
|
||||
import { useSelection } from '@/components/AnTable/plugins/useSelectionPlugin';
|
||||
import { useRefresh } from '@/components/AnTable/plugins/useRefreshPlugin';
|
||||
import { useColumnConfig } from '@/components/AnTable/plugins/useColumnConfig';
|
||||
import { Ref } from 'vue';
|
||||
import { Button, Message } from '@arco-design/web-vue';
|
||||
import { delConfirm, sleep } from '@/utils';
|
||||
|
||||
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
|
||||
type="primary"
|
||||
status="danger"
|
||||
disabled={!selected.value.length}
|
||||
loading={loading.value}
|
||||
onClick={onClick}
|
||||
>
|
||||
批量删除
|
||||
</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-import"></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!] ?? '-';
|
||||
}
|
||||
},
|
||||
};
|
||||
})(),
|
||||
],
|
||||
data(search) {
|
||||
return api.user.getUsers(search);
|
||||
},
|
||||
paging: {
|
||||
hide: false,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
dataIndex: 'id',
|
||||
title: 'ID',
|
||||
configable: false,
|
||||
},
|
||||
{
|
||||
dataIndex: 'nickname',
|
||||
title: '用户名称',
|
||||
},
|
||||
{
|
||||
dataIndex: 'username',
|
||||
title: '登录账号',
|
||||
},
|
||||
{
|
||||
dataIndex: 'email',
|
||||
title: '邮箱',
|
||||
},
|
||||
{
|
||||
dataIndex: 'phone',
|
||||
title: '手机号码',
|
||||
},
|
||||
{
|
||||
dataIndex: 'createdBy',
|
||||
title: '创建人',
|
||||
},
|
||||
{
|
||||
dataIndex: 'createdAt',
|
||||
title: '创建时间',
|
||||
},
|
||||
{
|
||||
dataIndex: 'updatedBy',
|
||||
title: '更新人',
|
||||
},
|
||||
{
|
||||
dataIndex: 'updatedAt',
|
||||
title: '更新时间',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
width: 180,
|
||||
configable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: '修改',
|
||||
},
|
||||
{
|
||||
text: '移动',
|
||||
// visible: () => false,
|
||||
},
|
||||
{
|
||||
text: '删除',
|
||||
disable: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
search: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名称',
|
||||
setter: 'input',
|
||||
},
|
||||
],
|
||||
create: {
|
||||
title: '新增',
|
||||
modalProps: {
|
||||
width: 111,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'title',
|
||||
label: '标题',
|
||||
setter: 'input',
|
||||
},
|
||||
],
|
||||
submit: async model => {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { component: UpForm, model: emodel } = useForm({
|
||||
formProps: {
|
||||
class: 'grid! grid-cols-2 gap-x-8',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'id',
|
||||
label: '输入组件',
|
||||
setter: 'input',
|
||||
setterSlots: {
|
||||
prefix: () => <span>123</span>,
|
||||
},
|
||||
itemSlots: {
|
||||
help: props => props.item.label,
|
||||
extra: () => 'extra',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'todo',
|
||||
label: '测试',
|
||||
},
|
||||
{
|
||||
field: 'xsa',
|
||||
label: '动态渲染',
|
||||
setter: 'input',
|
||||
visible: props => props.model.id,
|
||||
},
|
||||
{
|
||||
field: 'fsa',
|
||||
label: '动态禁止',
|
||||
setter: 'input',
|
||||
disable: props => props.model.id,
|
||||
},
|
||||
{
|
||||
field: 'sgs',
|
||||
label: '校验规则',
|
||||
setter: 'input',
|
||||
// required: true,
|
||||
rules: ['email'],
|
||||
},
|
||||
{
|
||||
field: 'sgss',
|
||||
label: '动态规则',
|
||||
setter: 'input',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '必须项',
|
||||
disable: props => !props.model.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'num',
|
||||
value: 20,
|
||||
label: '数字组件',
|
||||
setter: 'number',
|
||||
},
|
||||
{
|
||||
field: 'date',
|
||||
label: '日期组件',
|
||||
setter: 'date',
|
||||
},
|
||||
{
|
||||
field: '[startDate,endDate]',
|
||||
label: '字段语法',
|
||||
setter: 'dateRange',
|
||||
},
|
||||
{
|
||||
field: '{value,dd}',
|
||||
value: { value: 1 },
|
||||
label: '下拉组件',
|
||||
setter: 'select',
|
||||
options: [
|
||||
{
|
||||
label: '测试',
|
||||
value: {
|
||||
value: 1,
|
||||
dd: 123,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '测试2',
|
||||
value: {
|
||||
value: 2,
|
||||
dd: 223,
|
||||
},
|
||||
},
|
||||
],
|
||||
setterProps: {
|
||||
valueKey: 'value',
|
||||
},
|
||||
},
|
||||
],
|
||||
async submit(model) {
|
||||
return { message: '操作成功' };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
|
|
@ -24,3 +324,4 @@
|
|||
}
|
||||
}
|
||||
</route>
|
||||
@/components/AnForm/components/useFormModel
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import { useProgressGard } from "../guards/progress";
|
|||
import { useTitleGuard } from "../guards/title";
|
||||
import { baseRoutes } from "../routes/base";
|
||||
import { historyMode } from "./util";
|
||||
import { routes } from "../routes/page";
|
||||
|
||||
/**
|
||||
* 路由实例
|
||||
*/
|
||||
export const router = createRouter({
|
||||
history: historyMode(),
|
||||
routes: [...baseRoutes],
|
||||
routes: [...baseRoutes, ...routes],
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ body {
|
|||
overflow: hidden;
|
||||
}
|
||||
.arco-modal-header {
|
||||
background: var(--color-fill-3);
|
||||
border-bottom: none;
|
||||
// background: var(--color-fill-3);
|
||||
// border-bottom: none;
|
||||
}
|
||||
.arco-modal-footer {
|
||||
padding-top: 0;
|
||||
|
|
|
|||
|
|
@ -7,29 +7,22 @@ export {}
|
|||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
AAlert: typeof import('@arco-design/web-vue')['Alert']
|
||||
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
|
||||
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||
ACard: typeof import('@arco-design/web-vue')['Card']
|
||||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
||||
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
|
||||
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
||||
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
|
||||
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
||||
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
||||
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
||||
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
|
||||
ADropdownButton: typeof import('@arco-design/web-vue')['DropdownButton']
|
||||
AEmpty: typeof import('@arco-design/web-vue')['Empty']
|
||||
AForm: typeof import('@arco-design/web-vue')['Form']
|
||||
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
||||
AImage: typeof import('@arco-design/web-vue')['Image']
|
||||
AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview']
|
||||
AInput: typeof import('@arco-design/web-vue')['Input']
|
||||
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
|
||||
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
|
||||
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
|
||||
ALayout: typeof import('@arco-design/web-vue')['Layout']
|
||||
|
|
@ -45,19 +38,10 @@ declare module '@vue/runtime-core' {
|
|||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||
AniEmpty: typeof import('./../components/empty/AniEmpty.vue')['default']
|
||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
||||
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
||||
ARadio: typeof import('@arco-design/web-vue')['Radio']
|
||||
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
||||
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
||||
ASelect: typeof import('@arco-design/web-vue')['Select']
|
||||
ASpace: typeof import('@arco-design/web-vue')['Space']
|
||||
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
||||
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']
|
||||
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
|
||||
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
||||
ATree: typeof import('@arco-design/web-vue')['Tree']
|
||||
AUpload: typeof import('@arco-design/web-vue')['Upload']
|
||||
|
|
@ -68,7 +52,6 @@ declare module '@vue/runtime-core' {
|
|||
DragResizer: typeof import('./../components/editor/components/DragResizer.vue')['default']
|
||||
Editor: typeof import('./../components/editor/components/Editor.vue')['default']
|
||||
EditorPreview: typeof import('./../components/editor/components/EditorPreview.vue')['default']
|
||||
Empty: typeof import('./../components/empty/index.vue')['default']
|
||||
ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default']
|
||||
InputColor: typeof import('./../components/editor/components/InputColor.vue')['default']
|
||||
InputImage: typeof import('./../components/editor/components/InputImage.vue')['default']
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ declare module "*.vue" {
|
|||
export default component;
|
||||
}
|
||||
|
||||
declare module 'numeral' {
|
||||
const numeral: any
|
||||
declare module "numeral" {
|
||||
const numeral: any;
|
||||
export default numeral;
|
||||
}
|
||||
}
|
||||
|
||||
type Recordable = Record<string, any>;
|
||||
Loading…
Reference in New Issue