From 6ce573fda70236e736dafa0135e998cdd8659bd0 Mon Sep 17 00:00:00 2001 From: juetan Date: Tue, 18 Jul 2023 21:54:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 - src/components/form/form-item.tsx | 64 ++----- src/components/form/form-node.tsx | 36 ++-- src/components/form/form-rules.ts | 46 +++++ src/components/form/form.tsx | 34 ++-- src/components/form/use-form.tsx | 4 +- src/components/table/table.config.tsx | 119 ++++++------ src/components/table/table.tsx | 170 ++++++------------ src/components/table/use-table.tsx | 61 +++---- src/pages/user/index.vue | 18 +- src/style/{arco-design.less => css-arco.less} | 0 src/style/{style.less => css-base.less} | 0 .../{transition.less => css-transition.less} | 0 src/style/{uno.less => css-unocss.less} | 0 src/style/index.ts | 8 +- src/{config => utils}/defineConstants.ts | 0 vite.config.ts | 2 +- 17 files changed, 253 insertions(+), 310 deletions(-) create mode 100644 src/components/form/form-rules.ts rename src/style/{arco-design.less => css-arco.less} (100%) rename src/style/{style.less => css-base.less} (100%) rename src/style/{transition.less => css-transition.less} (100%) rename src/style/{uno.less => css-unocss.less} (100%) rename src/{config => utils}/defineConstants.ts (100%) diff --git a/.env b/.env index b39a0fd..1e21c32 100644 --- a/.env +++ b/.env @@ -24,7 +24,6 @@ VITE_API_BASE_URL = http://127.0.0.1:3030 VITE_API_PROXY_URL = /api # API文档地址(开发环境) 备注:需为openapi规范的json文件 -# VITE_API_DOCS_URL = http://127.0.0.1:3030/openapi-json VITE_API_DOCS_URL = https://petstore.swagger.io/v2/swagger.json # 端口号(开发环境) diff --git a/src/components/form/form-item.tsx b/src/components/form/form-item.tsx index e799c00..aa974f9 100644 --- a/src/components/form/form-item.tsx +++ b/src/components/form/form-item.tsx @@ -1,52 +1,8 @@ import { FormItem as BaseFormItem, FieldRule, FormItemInstance, SelectOptionData } from "@arco-design/web-vue"; import { NodeType, NodeUnion, nodeMap } from "./form-node"; +import { RuleMap } from "./form-rules"; -const defineRuleMap = >(ruleMap: T) => ruleMap; - -const ruleMap = 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 ruleMap; +export type FieldStringRule = keyof typeof RuleMap; export type FieldObjectRule = FieldRule & { disable?: (arg: { item: IFormItem; model: Record }) => boolean; @@ -67,11 +23,11 @@ export const FormItem = (props: any, { emit }: any) => { const rules = computed(() => { const result = []; if (item.required) { - result.push(ruleMap.required); + result.push(RuleMap.required); } item.rules?.forEach((rule: any) => { if (typeof rule === "string") { - result.push(ruleMap[rule as FieldStringRule]); + result.push(RuleMap[rule as FieldStringRule]); return; } if (!rule.disable) { @@ -144,9 +100,9 @@ type FormItemBase = { /** * 初始值 - * @description 默认值为undefined,优先级比model中的同名属性高。 + * @description 若指定该参数,将覆盖model中的同名属性。 */ - initialValue?: any; + initial?: any; /** * 标签名 @@ -172,14 +128,16 @@ type FormItemBase = { * @example * ```typescript * rules: [ - * 'idcard', // 内置的身份证号校验规则 + * // 内置 + * 'idcard', + * // 自定义 * { * match: /\d+/, * message: '请输入数字', * }, * ] - *``` - * @see https://arco.design/vue/component/form#FieldRule + *``` + * @see https://arco.design/vue/component/form#FieldRule */ rules?: FieldRuleType[]; diff --git a/src/components/form/form-node.tsx b/src/components/form/form-node.tsx index 61d7f5e..187c76c 100644 --- a/src/components/form/form-node.tsx +++ b/src/components/form/form-node.tsx @@ -17,29 +17,27 @@ import { const initOptions = ({ item, model }: any) => { if (Array.isArray(item.options)) { item.nodeProps.options = item.options; - return; } - if (typeof item.options !== "function") { - return; + if (typeof item.options === "function") { + item.nodeProps.options = reactive([]); + const fetchData = item.options; + item._updateOptions = async () => { + let data = await fetchData({ item, model }); + if (Array.isArray(data?.data)) { + data = data.data.map((i: any) => ({ label: i.name, value: i.id })); + } + if (Array.isArray(data)) { + item.nodeProps.options.splice(0); + item.nodeProps.options.push(...data); + } + }; + item._updateOptions(); } - item.nodeProps.options = reactive([]); - const fetchData = item.options; - item._updateOptions = async () => { - let data = await fetchData({ item, model }); - if (Array.isArray(data?.data)) { - data = data.data.map((i: any) => ({ label: i.name, value: i.id })); - } - if (Array.isArray(data)) { - item.nodeProps.options.splice(0); - item.nodeProps.options.push(...data); - } - }; - item._updateOptions(); }; -const defineNodeMap = (map: T) => { - return map -} +const defineNodeMap = (map: T) => { + return map; +}; /** * 表单项组件映射 diff --git a/src/components/form/form-rules.ts b/src/components/form/form-rules.ts new file mode 100644 index 0000000..ab02694 --- /dev/null +++ b/src/components/form/form-rules.ts @@ -0,0 +1,46 @@ +import { FieldRule } from "@arco-design/web-vue"; + +const defineRuleMap = >(ruleMap: T) => ruleMap; + +export const RuleMap = 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: "至少包含大写字母、小写字母、数字和特殊字符", + }, +}); diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index 42f7104..3ff546f 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -4,6 +4,8 @@ import { PropType } from "vue"; import { FormItem, IFormItem } from "./form-item"; import { NodeType, nodeMap } from "./form-node"; +type SubmitFn = (arg: { model: Record; items: IFormItem[] }) => Promise; + /** * 表单组件 */ @@ -28,7 +30,7 @@ export const Form = defineComponent({ * 提交表单 */ submit: { - type: Function as PropType<(arg: { model: Record; items: IFormItem[] }) => Promise>, + type: Function as PropType, }, /** * 传给Form组件的参数 @@ -68,6 +70,21 @@ export const Form = defineComponent({ return model; }; + const setModel = (model: Record) => { + for (const key of Object.keys(props.model)) { + if (/[^:]+:[^:]+/.test(key)) { + const [key1, key2] = key.split(":"); + props.model[key] = [model[key1], model[key2]]; + } else { + props.model[key] = model[key]; + } + } + }; + + const resetModel = () => { + assign(props.model, model); + }; + const submitForm = async () => { if (await formRef.value?.validate()) { return; @@ -82,21 +99,6 @@ export const Form = defineComponent({ } }; - const resetModel = () => { - assign(props.model, model); - }; - - const setModel = (model: Record) => { - for (const key of Object.keys(props.model)) { - if (/.+:.+/.test(key)) { - const [key1, key2] = key.split(":"); - props.model[key] = [model[key1], model[key2]]; - } else { - props.model[key] = model[key]; - } - } - }; - return { formRef, loading, diff --git a/src/components/form/use-form.tsx b/src/components/form/use-form.tsx index cfd18d3..498b86c 100644 --- a/src/components/form/use-form.tsx +++ b/src/components/form/use-form.tsx @@ -34,13 +34,13 @@ export const useForm = (options: Options) => { } if (/(.+)\?(.+)/.test(item.field)) { const [field, condition] = item.field.split("?"); - model[field] = item.initialValue ?? model[item.field]; + model[field] = item.initial ?? model[item.field]; const params = new URLSearchParams(condition); for (const [key, value] of params.entries()) { model[key] = value; } } - model[item.field] = model[item.field] ?? item.initialValue; + model[item.field] = model[item.field] ?? item.initial; const _item = { ...item }; items.push(_item); }); diff --git a/src/components/table/table.config.tsx b/src/components/table/table.config.tsx index 84c4eac..c42686c 100644 --- a/src/components/table/table.config.tsx +++ b/src/components/table/table.config.tsx @@ -1,71 +1,62 @@ import { Button } from "@arco-design/web-vue"; import { IconRefresh, IconSearch } from "@arco-design/web-vue/es/icon"; -/** - * 搜索表单默认参数 - */ -export const TABLE_SEARCH_DEFAULTS = { - labelAlign: "left", - autoLabelWidth: true, - model: {}, -}; - -/** - * 表格列默认参数 - */ -export const TABLE_COLUMN_DEFAULTS = { - ellipsis: true, - tooltip: true, - render: ({ record, column }: any) => record[column.dataIndex] || "-", -}; - -/** - * 行操作按钮默认参数 - */ -export const TABLE_ACTION_DEFAULTS = { - buttonProps: { - type: "primary", +export const config = { + searchFormProps: { + labelAlign: "left", + autoLabelWidth: true, + model: {}, }, -}; - -/** - * 删除弹窗默认参数 - */ -export const TABLE_DELTE_DEFAULTS = { - title: "删除确认", - content: "确认删除当前数据吗?", - modalClass: "text-center", - hideCancel: false, - maskClosable: false, -}; - -export const TALBE_INDEX_DEFAULTS = { - title: "#", - width: 60, - align: "center", - render: ({ rowIndex }: any) => rowIndex + 1, -}; - -export const searchItem = { - field: "id", - type: "custom", - itemProps: { - class: "table-search-item col-start-4 !mr-0 grid grid-cols-[0_1fr]", - hideLabel: true, - }, - component: () => { - const tableRef = inject("ref:table"); - return ( -
- {(tableRef.search?.items?.length || 0) > 3 && ( - + )} + - )} - -
- ); + + ); + }, + }, + pagination: { + current: 1, + pageSize: 10, + total: 300, + showTotal: true, + }, + columnBase: { + ellipsis: true, + tooltip: true, + render: ({ record, column }: any) => record[column.dataIndex] || "-", + }, + columnIndex: { + title: "序号", + width: 60, + align: "center", + render: ({ rowIndex }: any) => rowIndex + 1, + }, + columnButtonBase: { + buttonProps: { + type: "primary", + }, + }, + columnButtonDelete: { + title: "删除确认", + content: "确认删除当前数据吗?", + modalClass: "text-center", + hideCancel: false, + maskClosable: false, }, }; diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index 738ce7d..b6b1fe3 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -1,11 +1,11 @@ -import { - TableColumnData as BaseColumn, - TableData as BaseData, - Table as BaseTable, - Divider, -} from "@arco-design/web-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, watch } from "vue"; import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form"; +import { config } from "./table.config"; + +type DataFn = (search: Record, paging: { page: number; size: number }) => Promise; +type Data = BaseData[] | DataFn; /** * 表格组件 @@ -18,9 +18,7 @@ export const Table = defineComponent({ * 表格数据 */ data: { - type: [Array, Function] as PropType< - BaseData[] | ((search: Record, paging: { page: number; size: number }) => Promise) - >, + type: [Array, Function] as PropType, }, /** * 表格列设置 @@ -34,7 +32,7 @@ export const Table = defineComponent({ */ pagination: { type: Object as PropType, - default: () => reactive({ current: 1, pageSize: 10, total: 300, showTotal: true }), + default: () => reactive(config.pagination), }, /** * 搜索表单配置 @@ -73,25 +71,17 @@ export const Table = defineComponent({ const createRef = ref(); const modifyRef = ref(); const renderData = ref([]); - const inlineSearch = computed(() => (props.search?.items?.length || 0) < 4); - - Object.assign(props.columns, { getInstance: () => getCurrentInstance() }); - - const getPaging = (pagination: Partial) => { - const { current: page, pageSize: size } = { ...props.pagination, ...pagination } as any; - return { page, size }; - }; + const inlined = computed(() => (props.search?.items?.length ?? 0) < 4); + const reloadData = () => loadData({ current: 1, pageSize: 10 }); + const openModifyModal = (data: any) => modifyRef.value?.open(data.record); const loadData = async (pagination: Partial = {}) => { - if (!props.data) { - return; - } + const merged = { ...props.pagination, ...pagination }; + const paging = { page: merged.current, size: merged.pageSize }; + const model = searchRef.value?.getModel() ?? {}; if (Array.isArray(props.data)) { - if (!props.search?.model) { - return; - } - const filters = Object.entries(props.search?.model || {}); - const data = props.data?.filter((item) => { + const filters = Object.entries(model); + const data = props.data.filter((item) => { return filters.every(([key, value]) => { if (typeof value === "string") { return item[key].includes(value); @@ -99,59 +89,35 @@ export const Table = defineComponent({ return item[key] === value; }); }); - renderData.value = data || []; + renderData.value = data; props.pagination.total = renderData.value.length; props.pagination.current = 1; - return; } - if (typeof props.data !== "function") { - return; + if (typeof props.data === "function") { + try { + loading.value = true; + const resData = await props.data(model, paging); + const { data = [], meta = {} } = resData || {}; + const { page: pageNum, total } = meta; + renderData.value = data; + props.pagination.total = total; + props.pagination.current = pageNum; + } catch (error) { + console.log("table error", error); + } finally { + loading.value = false; + } } - const model = searchRef.value?.getModel() || {}; - const paging = getPaging(pagination); - try { - loading.value = true; - const resData = await props.data(model, paging); - const { data = [], meta = {} } = resData || {}; - const { page: pageNum, total } = meta; - renderData.value = data; - Object.assign(props.pagination, { current: pageNum, total }); - } catch (error) { - console.log("table error", error); - } finally { - loading.value = false; - } - }; - - const reloadData = () => { - loadData({ current: 1, pageSize: 10 }); - }; - - const openModifyModal = (data: any) => { - modifyRef.value?.open(data.record); - }; - - const onPageChange = (current: number) => { - loadData({ current }); - }; - - const onCreateOk = () => { - reloadData(); - }; - - const onModifyOk = () => { - reloadData(); }; watch( () => props.data, (data) => { - if (!Array.isArray(data)) { - return; + if (Array.isArray(data)) { + renderData.value = data; + props.pagination.total = data.length; + props.pagination.current = 1; } - renderData.value = data; - props.pagination.total = data.length; - props.pagination.current = 1; }, { immediate: true, @@ -162,19 +128,20 @@ export const Table = defineComponent({ loadData(); }); + if (props.search) { + merge(props.search, { formProps: { layout: "inline" } }); + } + const state = { loading, + inlined, searchRef, createRef, modifyRef, renderData, - inlineSearch, loadData, reloadData, openModifyModal, - onPageChange, - onCreateOk, - onModifyOk, }; provide("ref:table", { ...state, ...props }); @@ -185,52 +152,33 @@ export const Table = defineComponent({ (this.columns as any).instance = this; return (
- {!this.inlineSearch && ( -
-
(this.searchRef = el)} class="grid grid-cols-4 gap-x-4" {...this.search}>
+ {!this.inlined && ( +
+
)} - {!this.inlineSearch && } -
+ +
- {this.create && ( - (this.createRef = el)} - onOk={this.onCreateOk} - {...(this.create as any)} - > - )} + {this.create && } {this.modify && ( - (this.modifyRef = el)} - onOk={this.onModifyOk} - trigger={false} - {...(this.modify as any)} - > + )} {this.$slots.action?.()}
-
- {this.inlineSearch && ( -
(this.searchRef = el)} - {...{ ...this.search, formProps: { layout: "inline" } }} - >
- )} -
-
-
- +
{this.inlined &&
}
+ + this.loadData({ current })} + >
); }, diff --git a/src/components/table/use-table.tsx b/src/components/table/use-table.tsx index a2a7316..44fc4d8 100644 --- a/src/components/table/use-table.tsx +++ b/src/components/table/use-table.tsx @@ -1,14 +1,8 @@ import { Link, Message, Modal, TableColumnData } from "@arco-design/web-vue"; -import { defaultsDeep, isArray, isFunction, mergeWith, omit } from "lodash-es"; +import { defaultsDeep, isArray, isFunction, mergeWith } from "lodash-es"; import { reactive } from "vue"; import { TableInstance } from "./table"; -import { - TABLE_ACTION_DEFAULTS, - TABLE_COLUMN_DEFAULTS, - TABLE_DELTE_DEFAULTS, - TALBE_INDEX_DEFAULTS, - searchItem, -} from "./table.config"; +import { config } from "./table.config"; import { UseTableOptions } from "./use-interface"; const merge = (...args: any[]) => { @@ -34,18 +28,15 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)) const getTable = (): TableInstance => (columns as any).instance; + /** + * 表格列处理 + */ options.columns.forEach((column) => { - // 序号 if (column.type === "index") { - defaultsDeep(column, TALBE_INDEX_DEFAULTS); + defaultsDeep(column, config.columnIndex); } - // 操作 if (column.type === "button" && isArray(column.buttons)) { - if (options.detail) { - column.buttons.unshift({ text: "详情", onClick: (data) => {} }); - } - if (options.modify) { const modifyAction = column.buttons.find((i) => i.type === "modify"); if (modifyAction) { @@ -68,11 +59,10 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)) column.buttons = column.buttons?.map((action) => { let onClick = action?.onClick; - if (action.type === "delete") { onClick = (data) => { Modal.warning({ - ...TABLE_DELTE_DEFAULTS, + ...config.columnButtonDelete, onOk: async () => { const resData: any = await action?.onClick?.(data); resData.msg && Message.success(resData?.msg || ""); @@ -81,27 +71,26 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)) }); }; } - - return { ...TABLE_ACTION_DEFAULTS, ...action, onClick } as any; + return { ...config.columnButtonBase, ...action, onClick } as any; }); - column.render = (columnData) => - column.buttons?.map((action) => { - const onClick = () => action.onClick?.(columnData); - const omitKeys = ["text", "render", "api", "action", "onClick", "disabled"]; - const disabled = () => action.disabled?.(columnData); - if (action.visible && !action.visible(columnData)) { + column.render = (columnData) => { + return column.buttons?.map((btn) => { + const onClick = () => btn.onClick?.(columnData); + const disabled = () => btn.disabled?.(columnData); + if (btn.visible && !btn.visible(columnData)) { return null; } return ( - - {action.text} + + {btn.text} ); }); + }; } - columns.push({ ...TABLE_COLUMN_DEFAULTS, ...column }); + columns.push({ ...config.columnBase, ...column }); }); const itemsMap = options.common?.items?.reduce((map, item) => { @@ -114,28 +103,34 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)) */ if (options.search && options.search.items) { const searchItems: any[] = []; - options.search.items.forEach((item) => { + for (const item of options.search.items) { if (typeof item === "string") { if (!itemsMap[item]) { throw new Error(`search item ${item} not found in common items`); } searchItems.push(itemsMap[item]); - return; + continue; } if ("extend" in item && item.extend && itemsMap[item.extend]) { searchItems.push(merge({}, itemsMap[item.extend], item)); - return; + continue; } searchItems.push(item); - }); - searchItems.push(searchItem); + } + searchItems.push(config.searchItemSubmit); options.search.items = searchItems; } + /** + * 新增表单处理 + */ if (options.create && propTruly(options.create, "extend")) { options.create = merge(options.common, options.create); } + /** + * 修改表单处理 + */ if (options.modify && propTruly(options.modify, "extend")) { options.modify = merge(options.common, options.modify); } diff --git a/src/pages/user/index.vue b/src/pages/user/index.vue index 058ad5d..df3fdb3 100644 --- a/src/pages/user/index.vue +++ b/src/pages/user/index.vue @@ -5,12 +5,14 @@