Compare commits

...

9 Commits

Author SHA1 Message Date
luoer 2b5c367117 feat: 添加表格插件系统
自动部署 / build (push) Failing after 58s Details
2023-11-17 17:38:34 +08:00
绝弹 1f7c1a95b3 feat: 优化表格列设置 2023-11-17 09:02:23 +08:00
luoer 85b781d946 feat: 添加表格列设置组件 2023-11-16 17:44:58 +08:00
绝弹 17c695d065 feat: 优化表单提示 2023-11-15 21:52:12 +08:00
luoer eeed362320 feat: 重构表单 2023-11-15 17:58:04 +08:00
luoer 72fd7eba25 feat: 临时提交 2023-11-14 17:41:22 +08:00
绝弹 51e287c747 feat: 临时提交 2023-11-13 22:01:00 +08:00
luoer 71baafecc7 feat: 添加表单钩子 2023-11-13 16:59:14 +08:00
luoer a2c263cef7 feat: 临时提交 2023-11-10 17:45:47 +08:00
99 changed files with 4612 additions and 57 deletions

4
.env
View File

@ -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(锚点)

View File

@ -1,5 +1,7 @@
{
"tabWidth": 2,
"printWidth": 120,
"bracketSpacing": true
"bracketSpacing": true,
"singleQuote": true,
"arrowParens": "avoid"
}

View File

@ -13,6 +13,11 @@ declare module "axios" {
* @private
*/
closeToast?: () => void;
/**
*
* @default false
*/
msg?: boolean | string;
/**
*
*/

View File

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

View File

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

View File

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

View File

@ -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,
},
/**
* Modalprops
*/
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"
>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export * from './components/Form';
export * from './hooks/useForm';
export * from './components/useFormContext';
export * from './components/FormItem';
export * from './components/useFormModel';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: '请输入',
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './hooks/useTable';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -230,4 +230,4 @@ export type NodeUnion = {
*/
nodeProps?: NodeMap[key]["nodeProps"];
};
}[NodeType];
}[NodeType];

View File

@ -43,4 +43,4 @@ export const RuleMap = defineRuleMap({
match: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/,
message: "至少包含大写字母、小写字母、数字和特殊字符",
},
});
});

View File

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

View File

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

View File

@ -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 FormItemlabel
*/
label?: string | IAnFormItemElemFn;
/**
*
* @description FormItemhelp
*/
help?: string | IAnFormItemElemFn;
/**
*
* @description FormItemextra
*/
extra?: string | IAnFormItemElemFn;
/**
*
* @description false
*/
required?: boolean;
/**
*
* @description ()
*/
rules?: Rule[];
/**
* `FormItem`
* @description fieldlabelrequiredrulesdisabled
*/
itemProps?: Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">;
/**
*
* @description
*/
visible?: IAnFormItemBoolFn;
/**
*
* @description
*/
disable?: IAnFormItemBoolFn;
};
/**
*
*/
export type FormItem = BaseFormItem & NodeUnion;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [],
// },
// });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,4 +31,4 @@ export function setModel(model: any, data: Record<string, any>) {
model[key] = data[key];
}
}
}
}

View File

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

View File

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

View File

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

13
src/hooks/useVisible.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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