From 9a15a88eb00681d249017c38864cb62e66ddf23e Mon Sep 17 00:00:00 2001 From: luoer Date: Fri, 24 Nov 2023 17:14:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E6=97=A7=E7=9A=84?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E8=A1=A8=E5=8D=95=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 6 +- scripts/vite/plugin.ts | 16 +- src/api/interceptors/exception.ts | 2 +- src/components/AnForm/hooks/useFormModal.tsx | 2 +- src/components/form/README.md | 237 --------------- src/components/form/form-config.ts | 113 ------- src/components/form/form-item.tsx | 180 ----------- src/components/form/form-modal.tsx | 194 ------------ src/components/form/form-node.tsx | 233 -------------- src/components/form/form-rules.ts | 46 --- src/components/form/form.tsx | 120 -------- src/components/form/index.ts | 4 - src/components/form/use-form-modal.tsx | 61 ---- src/components/form/use-form.tsx | 59 ---- src/components/form/util.ts | 34 --- src/components/index.ts | 2 - src/components/table/README.md | 162 ---------- src/components/table/colume.tsx | 38 --- src/components/table/index.ts | 4 - src/components/table/table.config.tsx | 78 ----- src/components/table/table.tsx | 213 ------------- src/components/table/use-interface.ts | 164 ---------- src/components/table/use-table.tsx | 287 ------------------ src/pages/_layout/components/userDropdown.vue | 57 ++-- src/pages/_layout/index.vue | 41 ++- src/pages/_login/index.vue | 4 +- src/pages/content/comment/index.vue | 121 ++++---- .../content/media/components/AnGroup.vue | 33 +- .../content/media/components/AnUpload.vue | 2 +- src/pages/content/media/index.vue | 83 ++--- src/pages/content/post/index.vue | 43 ++- src/pages/home/index.vue | 9 +- src/pages/system/logo/index.vue | 72 ++--- src/pages/system/menu/index.vue | 177 +++++------ src/pages/system/role/index.vue | 127 ++++---- src/pages/system/user/components/password.tsx | 26 -- src/pages/system/user/index.vue | 48 +-- src/styles/css-arco.less | 4 + 38 files changed, 402 insertions(+), 2700 deletions(-) delete mode 100644 src/components/form/README.md delete mode 100644 src/components/form/form-config.ts delete mode 100644 src/components/form/form-item.tsx delete mode 100644 src/components/form/form-modal.tsx delete mode 100644 src/components/form/form-node.tsx delete mode 100644 src/components/form/form-rules.ts delete mode 100644 src/components/form/form.tsx delete mode 100644 src/components/form/index.ts delete mode 100644 src/components/form/use-form-modal.tsx delete mode 100644 src/components/form/use-form.tsx delete mode 100644 src/components/form/util.ts delete mode 100644 src/components/index.ts delete mode 100644 src/components/table/README.md delete mode 100644 src/components/table/colume.tsx delete mode 100644 src/components/table/index.ts delete mode 100644 src/components/table/table.config.tsx delete mode 100644 src/components/table/table.tsx delete mode 100644 src/components/table/use-interface.ts delete mode 100644 src/components/table/use-table.tsx delete mode 100644 src/pages/system/user/components/password.tsx diff --git a/.env b/.env index 5b92815..b1a2f37 100644 --- a/.env +++ b/.env @@ -2,13 +2,13 @@ # 应用配置 # ===================================================================================== # 网站标题 -VITE_TITLE = Appnify +VITE_TITLE = 绝弹管理中心 # 网站副标题 -VITE_SUBTITLE = 快速开发web应用的模板工具 +VITE_SUBTITLE = 绝弹管理中心 # 部署路径: 当为 ./ 时路由模式需为 hash VITE_BASE = / # 接口前缀:参见 axios 的 baseURL -VITE_API = https://appnify.app.juetan.cn/ +VITE_API = http://127.0.0.1:3030/ # 首页路径 VITE_HOME_PATH = /home # 路由模式:web(路径) hash(锚点) diff --git a/scripts/vite/plugin.ts b/scripts/vite/plugin.ts index 6c2cbf9..3ef29c9 100644 --- a/scripts/vite/plugin.ts +++ b/scripts/vite/plugin.ts @@ -7,14 +7,12 @@ import pkg from "../../package.json"; * 项目 logo * @description 内容:APPTIFY */ -const LOGO = ` - ________ ______ ______ _________ ________ ______ __ __ -/_______/\\\\ /_____/\\\\ /_____/\\\\ /________/\\\\/_______/\\\\/_____/\\\\ /_/\\\\/_/\\\\ -\\\\::: _ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\__.::.__\\\\/\\\\__.::._\\\\/\\\\::::_\\\\/_\\\\ \\\\ \\\\ \\\\ \\\\ - \\\\::(_) \\\\ \\\\\\\\:(_) \\\\ \\\\\\\\:(_) \\\\ \\\\ \\\\::\\\\ \\\\ \\\\::\\\\ \\\\ \\\\:\\\\/___/\\\\\\\\:\\\\_\\\\ \\\\ \\\\ - \\\\:: __ \\\\ \\\\\\\\: ___\\\\/ \\\\: ___\\\\/ \\\\::\\\\ \\\\ _\\\\::\\\\ \\\\__\\\\:::._\\\\/ \\\\::::_\\\\/ - \\\\:.\\\\ \\\\ \\\\ \\\\\\\\ \\\\ \\\\ \\\\ \\\\ \\\\ \\\\::\\\\ \\\\ /__\\\\::\\\\__/\\\\\\\\:\\\\ \\\\ \\\\::\\\\ \\\\ - \\\\__\\\\/\\\\__\\\\/ \\\\_\\\\/ \\\\_\\\\/ \\\\__\\\\/ \\\\________\\\\/ \\\\_\\\\/ \\\\__\\\\/ +const LOGO = ` _ _______ _______ ____ _____ _____ ________ ____ ____ + / \\\\ |_ __ \\\\|_ __ \\\\|_ \\\\|_ _||_ _||_ __ ||_ _||_ _| + / _ \\\\ | |__) | | |__) | | \\\\ | | | | | |_ \\\\_| \\\\ \\\\ / / + / ___ \\\\ | ___/ | ___/ | |\\\\ \\\\| | | | | _| \\\\ \\\\/ / + _/ / \\\\ \\\\_ _| |_ _| |_ _| |_\\\\ |_ _| |_ _| |_ _| |_ +|____| |____||_____| |_____| |_____|\\\\____||_____||_____| |______| `; /** @@ -47,7 +45,7 @@ const getBuildInfo = async () => { const latestTag = await exec("git describe --tags --abbrev=0"); const commits = await exec(`git rev-list --count ${latestTag}..HEAD`); const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`; - const content = ` 欢迎访问!版本: ${version} 标识: ${hash} 构建时间: ${time}`; + const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`; const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`; const script = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`; return script; diff --git a/src/api/interceptors/exception.ts b/src/api/interceptors/exception.ts index 572125d..00e7ddd 100644 --- a/src/api/interceptors/exception.ts +++ b/src/api/interceptors/exception.ts @@ -40,7 +40,7 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (. logoutTipShowing = true; Notification.warning({ title: '登陆提示', - content: '当前登陆已过期,请重新登陆!', + content: '登陆已过期,请重新登陆!', onClose: () => (logoutTipShowing = false), }); exipreHandler?.(error); diff --git a/src/components/AnForm/hooks/useFormModal.tsx b/src/components/AnForm/hooks/useFormModal.tsx index 3c0d6f8..6825427 100644 --- a/src/components/AnForm/hooks/useFormModal.tsx +++ b/src/components/AnForm/hooks/useFormModal.tsx @@ -67,7 +67,7 @@ export function useFormModal(options: FormModalUseOptions) { (modalRef.value = el)} title={props.title} - trigger={props.title} + trigger={props.trigger} modalProps={props.modalProps as any} model={props.model} items={props.items} diff --git a/src/components/form/README.md b/src/components/form/README.md deleted file mode 100644 index 4faecf1..0000000 --- a/src/components/form/README.md +++ /dev/null @@ -1,237 +0,0 @@ -### 介绍 -基于`Arco-Design`组件封装的表单,旨在通过较少的配置提升开发效率,将一些通过的状态管理内置,使得开发者只需关注核心内容即可快速开发通用型表单。 - -本表单适用于通用型表单,对于自定义要求较高的需求,可能不太适合。 - -### 功能 -- 配置化编写代码,保证UI一致性,提供开发效率。 -- 提供typesciprt类型提示 -- 表单项和校验规则之间可联动、可动态显示/隐藏 -- 内置常用校验规则,开箱即用 -- 支持组件参数透传,让每个组件都能自定义。 - -### 基本功能 -基本用法: -```tsx - - - -``` -以上, 只有四个参数,只需定义关注的内容,剩下的内容如内部状态等, 由表单管理。 -| 参数 | 说明 | 类型 | -| :--- | :--- | :--- | -| model | 表单数据(可选),默认从`items`每一项的`field`和`initialValue`生成,如果存在同名属性,将与其合并。 | `Record` | -| items | 表单项,具体用法看下文。| `FormItem[]` | -| submit | 提交表单的函数,可为同步/异步函数。当有返回值且返回值为包含`message`的对象时,将弹出成功提示。| `({ model, items }) => Promise` | -| formProps | 传递给`AForm`组件的参数(可选),具体可参考`Arco-Design`的`Form`组件,部分参数不可用,如`model`等。 | `FormInstance['$props']` | - -### 表单数据 -`model`表示当前表单的数据,可为空。当使用`useForm`时,将从`items`中每一项的`field`和`initialValue`生成。如果`model`中的属性与`field`值同名,且`initialValue`值不为空,则原`model`中的同名属性值将被覆盖。 - -对于日期范围框、级联选择器等值为数组的组件,提供有一份便捷的语法,请看如下示例: -```typescript -const form = useForm({ - items: [ - { - field: `[startDate, endDate]`, - label: '日期范围', - type: 'dateRange', - }, - { - field: '[provice: number, city: number, town: number]', - label: '省市区', - type: 'cascader', - options: [] - } - ] -}) -``` -以上,`field` 使用的是类似Typescript元组的写法,类型目前支持 number 和 boolean,在提交时将得到如下数据: -```typescript -{ - startDate: '2023', - endDate: '2024', - province: 1, - city: 2, - town: 3 -} -``` - -### 表单项 -用法示例: -```typescript -const form = useForm({ - items: [ - { - field: 'username', - initialValue: 'apptify', - - label: '用户名称', - type: 'input', - - itemProps: {}, - nodeProps: {}, - - visible: ({ model, item, items }) => true, - disable: ({ model, item, items }) => true, - - required: true, - rules: ['email'], - - options: ({ model }) => api.xx(model.id) - - component: ({ model, item, items }) =>
, - help: string | - } - ] -}) -``` -用法说明: -| 参数 | 说明 | 类型 | 默认值 | -| :--- | :--- | :--- | :--- | -| field | 字段名,将合并合并到`model`中,默认值为`undefined`,可通过`initalValue`指定初始值 | string | - | -| initialValue | 初始值, 作为默认初始值以及通过`formRef.reset`重置表单数据时的值 | any | undefined | -| label | 标签名,可为字符串或函数, 作用同`AFormItem`的`label`参数 | string \| ({ model,item }) => JSX.Element | - | -| type | 输入控件的类型,具体可参考下文 | NodeType | 'input' | -| visible | 动态控制该表单项是否显示 | boolean \| ({ model,item }) => boolean | - | -| disable | 动态控制该表单项是否禁止,作用同`FormItem`的`disabled`属性 | boolean \| ({ model, item }) => boolean | - | -| required | 是否必填,作用同`AFormItem`的`required`属性 | boolean | - | -| rules | 校验规则,内置常用规则,并支持动态生效,详见下文 | RuleType[] | - | -| options | 作用域`select`等多选项组件,支持动态获取 | (Option[]) \| ({ model, item }) => Option[] | - | -| itemProps | 传递给`AFormItem`组件的参数,部分参数不可用,如上面的`field`等参数 | FormItemInstance['$props'] | - | -| nodeProps | 传递给`type`属性对应组件的参数,如当`type`为`input`时, `nodeProps`类型为`Input`组件的props。 | NodeProps | - | - -### 控件类型 -表单项的`type`指定表单控件的类型,当输入具体的值时,`nodeProps`会提供对应的参数类型提示。内置有常见的组件,且带有默认的参数,具体默认参数可在`src/components/form/form-node.tsx`中查看: - -| 类型 | 说明 | -| :--- | :--- | -| input | 同 [Input](https://arco.design/vue/component/input) 组件 -| number | 同 [InputNumber](https://arco.design/vue/component/input-number) 组件 -| password | 同 [InputPassword](https://arco.design/vue/component/input#password) 组件 -| select | 同 [Select](https://arco.design/vue/component/select) 组件 -| time | 同 [TimePicker](https://arco.design/vue/component/time-picker) 组件 -| date | 同 [DatePicker](https://arco.design/vue/component/date-picker) 组件 -| dateRange | 同 [RangePicker](https://arco.design/vue/component/date-picker#range) 组件 -| textarea | 同 [Textarea](https://arco.design/vue/component/textarea) 组件 -| cascader | 同 [Cascader](https://arco.design/vue/component/cascader) 组件 -| checkbox | 同 [Checkbox](https://arco.design/vue/component/checkbox) 组件 -| radio | 同 [Radio](https://arco.design/vue/component/radio) 组件 -| slider | 同 [Slider](https://arco.design/vue/component/slider) 组件 -| submit | 提交表单按钮,应只有一个。 -| custom | 自定义组件,通过表单项的`component`属性定义,需返回一个JSX元素。 - -对于`select`、`checkbox`、`radio`和`cascader`类型,其`options`参数不通过`nodeProps`传递,而是写在表单项的`options`属性。该属性支持数组和函数类型,当为数组类型时将直接传递给控件,当为函数时可动态请求,返回值需为数组类型。 - -以上描述,示例如下: -```typescript -const form = useForm({ - items: [ - { - field: 'gender', - label: '性别', - type: 'select', - options: [ - { - label: '男', - value: 1, - }, - { - label: '女', - value: 2, - } - ] - }, - { - field: 'departmentId', - label: '部门', - type: 'cascader', - options: async ({ model, item }) => { - const res = await api.getDepartments(model.xx); - return res.data; - } - } - ] -}) -``` - -### 表单校验 - -跟表单校验相关的属性有2个,`required`(必填)和`rules`属性,其中`rules`内置常见的校验规则,参考如下: -| 校验规则 | 说明 | -| :--- | :--- | -| string | 格式为字符串 | -| number | 格式为数字 | -| passwod | 格式为密码类型,即至少包含大写字母、小写字母、数字和特殊字符。| -| required | 该项必填 | -| email | 格式为邮箱类型,例如: xx@abc.com | -| url | 格式为URL类型, 例如: https://abc.com | -| ip | 格式为IP类型, 例如: 101.10.10.302 | -| phone | 格式为11位手机号,例如: 15912345678 | -| idcard | 格式为18位身份证号,例如: 12345619991205131x | -| alphabet | 格式为26字母,例如:apptify | - -当以上规则不满足需求时,可通过对象自定义校验规则,具体语法可参考`AFormItem`的 [FieldRule](https://arco.design/vue/component/form#FieldRule) 文档。在其基础上,可添加一个`disable`函数,用于动态禁止/允许当前校验规则。 - -用法示例: -```typescript -const form = useForm({ - items: [ - { - required: true, - rules: [ - 'email', - { - match: /\d{2,3}/, - message: '请输入2~3位数字', - disable: ({ model, item, items }) => !model.username - } - ], - } - ] -}) -``` - -### 提交表单 -`submit`为提交表单的函数,通常返回一个`promise`,当该函数抛出异常,则默认为提交失败。该函数有一个可选的返回值,如果返回值为包含`message`的对象时,将弹出一个包含`message`值的成功提示。 - -示例如下: -```typescript -const form = useForm({ - submit: async ({ model, items }) => { - const res = await api.xx(model); - return { message: res.msg } - } -}) -``` - -### 常见问题 -- Q:为什么不是模板形式? -- A:状态驱动,配置式更易于描述逻辑,模板介入和引入的组件比较多,且对于做typescript类型提示不是很方便。 -- Q:为什么不是JSON形式? -- A:对于自定义组件支持、联动等不是非常友好,尽管可以通过解析字符串执行等方式实现,对typescript提示不是很友好。 - -### 最后 -尽管看起来是低代码,但其实我更倾向于是业务组件。 \ No newline at end of file diff --git a/src/components/form/form-config.ts b/src/components/form/form-config.ts deleted file mode 100644 index 8b486fc..0000000 --- a/src/components/form/form-config.ts +++ /dev/null @@ -1,113 +0,0 @@ -export const config = { - item: { - defaults: { - type: "input", - }, - }, - /** - * 获取API错误信息 - */ - getApiErrorMessage(error: any) { - return error?.response?.data?.message || error?.message || "Error"; - }, - /** - * 设置表单数据 - */ - setModel: function setModel(model: any, data: any) { - for (const key of Object.keys(model)) { - // 数组类型 - if (/^\[.+\]$/.test(key)) { - const subkeysStr = key.replaceAll(/\s/g, "").match(/^\[(.+)\]$/)?.[1]; - if (!subkeysStr) { - model[key] = data[key]; - continue; - } - const subkeys = subkeysStr.split(","); - const value = new Array(subkeys.length); - subkeys.forEach((subkey, index) => { - if (/.+:number$/.test(subkey)) { - subkey = subkey.replace(/:number$/, ""); - value[index] = Number(data[subkey]); - return; - } - if (/.+:boolean$/.test(subkey)) { - subkey = subkey.replace(/:boolean$/, ""); - value[index] = Boolean(data[subkey]); - return; - } - value[index] = data[subkey]; - }); - model[key] = value; - continue; - } - // 默认类型 - model[key] = data[key]; - } - return model; - }, - /** - * 获取表单数据 - */ - getModel: function getModel(model: Record) { - const data: any = {}; - 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; - }, -}; - -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; - item.nodeProps[key] = reactive([]); - item._updateOptions = async () => { - let data = await loadData({ item, model }); - if (Array.isArray(data?.data?.data)) { - data = data.data.data.map((i: any) => ({ - ...i, - label: i.name, - value: i.id, - })); - } - if (Array.isArray(data)) { - item.nodeProps[key].splice(0); - item.nodeProps[key].push(...data); - } - }; - item._updateOptions(); - } -} diff --git a/src/components/form/form-item.tsx b/src/components/form/form-item.tsx deleted file mode 100644 index 3742c3a..0000000 --- a/src/components/form/form-item.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { FormItem as BaseFormItem, FieldRule, FormItemInstance, SelectOptionData } from "@arco-design/web-vue"; -import { NodeType, NodeUnion, nodeMap } from "./form-node"; -import { RuleMap } from "./form-rules"; - -export type FieldStringRule = keyof typeof RuleMap; -export type FieldObjectRule = FieldRule & { - disable?: (arg: { item: IFormItem; model: Record }) => boolean; -}; -export type FieldRuleType = FieldStringRule | FieldObjectRule; - -/** - * 表单项 - */ -export const FormItem = (props: any, { emit }: any) => { - const { item } = props; - const args = { - ...props, - field: item.field, - }; - - const rules = computed(() => { - const result = []; - if (item.required) { - result.push(RuleMap.required); - } - item.rules?.forEach((rule: any) => { - if (typeof rule === "string") { - result.push(RuleMap[rule as FieldStringRule]); - return; - } - if (!rule.disable) { - result.push(rule); - return; - } - if (!rule.disable({ model: props.model, item, items: props.items })) { - result.push(rule); - } - }); - return result; - }); - - const disabled = computed(() => { - if (item.disable === undefined) { - return false; - } - if (typeof item.disable === "function") { - return item.disable(args); - } - return item.disable; - }); - - if (item.visible && !item.visible(args)) { - return null; - } - - return ( - - {{ - default: () => { - if (item.component) { - return ; - } - const comp = nodeMap[item.type as NodeType]?.component; - if (!comp) { - return null; - } - if (item.type === "submit") { - return emit("submit")} onCancel={emit("cancel")} />; - } - return ; - }, - label: item.label && (() => (typeof item.label === "string" ? item.label : item.label?.(args))), - help: item.help && (() => (typeof item.help === "string" ? item.help : item.help?.(args))), - extra: item.extra && (() => (typeof item.extra === "string" ? item.extra : item.extra?.(args))), - }} - - ); -}; - -type FormItemBase = { - /** - * 字段名,用于表单、校验和输入框绑定,支持特殊语法。 - * @example - * ```typescript - * { - * field: '[v1,v2]', - * type: 'dateRange', - * } - * // 将得到 - * { - * v1: '2021', - * v2: '2021', - * } - * ``` - */ - field: string; - - /** - * 初始值 - * @description 若指定该参数,将覆盖model中的同名属性。 - */ - initial?: any; - - /** - * 标签名 - * @description 同FormItem组件的label属性 - */ - label?: string | ((args: { item: IFormItem; model: Record }) => any); - - /** - * 传递给`FormItem`组件的参数 - * @description 部分属性会不可用,如field、label、required、rules、disabled等 - */ - itemProps?: Partial>; - - /** - * 是否必填 - * @description 默认值为false - */ - required?: boolean; - - /** - * 校验规则数组 - * @description 支持字符串(内置)、对象形式 - * @example - * ```typescript - * rules: [ - * // 内置 - * 'idcard', - * // 自定义 - * { - * match: /\d+/, - * message: '请输入数字', - * }, - * ] - *``` - * @see https://arco.design/vue/component/form#FieldRule - */ - rules?: FieldRuleType[]; - - /** - * 是否可见 - * @description 动态控制表单项是否可见 - */ - visible?: (arg: { item: IFormItem; model: Record }) => boolean; - - /** - * 是否禁用 - * @description 动态控制表单项是否禁用 - */ - disable?: (arg: { item: IFormItem; model: Record }) => boolean; - - /** - * 选项,数组或者函数 - * @description 用于下拉框、单选框、多选框等组件, 支持动态加载 - */ - options?: SelectOptionData[] | ((arg: { item: IFormItem; model: Record }) => Promise); - - /** - * 表单项内容的渲染函数 - * @description 用于自定义表单项内容 - */ - component?: (args: { item: IFormItem; model: Record; field: string }) => any; - - /** - * 帮助提示 - * @description 同FormItem组件的help插槽 - * @see https://arco.design/vue/component/form#form-item%20Slots - */ - help?: string | ((args: { item: IFormItem; model: Record }) => any); - - /** - * 额外内容 - * @description 同FormItem组件的extra插槽 - * @see https://arco.design/vue/component/form#form-item%20Slots - */ - extra?: string | ((args: { item: IFormItem; model: Record }) => any); -}; - -export type IFormItem = FormItemBase & NodeUnion; \ No newline at end of file diff --git a/src/components/form/form-modal.tsx b/src/components/form/form-modal.tsx deleted file mode 100644 index 24a51f1..0000000 --- a/src/components/form/form-modal.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { Button, ButtonInstance, FormInstance, Message, Modal } from "@arco-design/web-vue"; -import { assign, cloneDeep, omit } from "lodash-es"; -import { PropType, VNode, defineComponent } from "vue"; -import { Form } from "./form"; -import { config } from "./form-config"; -import { IFormItem } from "./form-item"; - -/** - * 表单弹窗组件 - */ -export const FormModal = defineComponent({ - name: "FormModal", - inheritAttrs: false, - props: { - /** - * 弹窗标题 - * @default '添加' - */ - title: { - type: [String, Function] as PropType< - string | ((args: { model: Record; items: IFormItem[] }) => string) - >, - default: "添加", - }, - /** - * 触发元素 - */ - trigger: { - type: [Boolean, Function, Object] as PropType< - | boolean - | ((props: { model: any; items: any[] }) => VNode) - | { - text?: string; - buttonProps?: ButtonInstance["$props"]; - } - >, - default: true, - }, - /** - * 传递给Modal组件的props - */ - modalProps: { - type: Object as PropType["$props"], "visible" | "title" | "onBeforeOk">>, - }, - /** - * 表单数据 - */ - model: { - type: Object as PropType>, - required: true, - }, - /** - * 表单各项 - */ - items: { - type: Array as PropType, - required: true, - }, - /** - * 提交表单的函数 - * @description 可返回`{ message }`类型,用于显示提示信息 - */ - submit: { - type: Function as PropType<(args: { model: any; items: IFormItem[] }) => PromiseLike>, - default: () => true, - }, - /** - * 传递给Form组件的props - */ - formProps: { - type: Object as PropType>, - }, - }, - emits: ["close", "submited"], - setup(props, { slots, emit, attrs }) { - const origin = cloneDeep(props.model); - const formRef = ref>(); - const loading = ref(false); - const visible = ref(false); - - const open = async (data: Record = {}) => { - visible.value = true; - await nextTick(); - config.setModel(props.model, data); - }; - - const onBeforeOk = async () => { - if (typeof attrs.onBeforeOk === "function") { - const isOk = await attrs.onBeforeOk(); - if (!isOk) return false; - } - const errors = await formRef.value?.formRef?.validate(); - if (errors) { - return false; - } - try { - const model = formRef.value?.getModel() || {}; - const res = await props.submit?.({ items: props.items, model }); - res?.data?.message && Message.success(`提示: ${res.data.message}`); - emit("submited", res); - } catch (error: any) { - const message = config.getApiErrorMessage(error); - if (message) { - Message.error(`提示: ${message}`); - } - return false; - } - return true; - }; - - const onClose = () => { - visible.value = false; - assign(props.model, origin); - emit("close"); - }; - - const modalTitle = computed(() => { - if (typeof props.title === "string") { - return props.title; - } - if (typeof props.title === "function") { - return props.title({ model: props.model, items: props.items }); - } - }); - - const modalTrigger = computed(() => { - if (!props.trigger) { - return null; - } - let content; - if (typeof props.trigger === "boolean" || typeof props.trigger === "string") { - content = ( - - ); - } - if (typeof props.trigger === "function") { - content = props.trigger({ model: props.model, items: props.items }); - } - if (typeof props.trigger === "object") { - content = ( - - ); - } - if (slots.trigger) { - content = slots.trigger({ model: props.model, items: props.items }); - } - return open()}>{content}; - }); - - return { - origin, - formRef, - loading, - visible, - modalTitle, - modalTrigger, - open, - onClose, - onBeforeOk, - }; - }, - render() { - return ( - <> - {this.modalTrigger} - - {this.visible && ( -
(this.formRef = el)} {...this.formProps} model={this.model} items={this.items}> - {{ ...this.$slots }} -
- )} -
- - ); - }, -}); - -export type FormModalInstance = InstanceType; -export type FormModalProps = FormModalInstance["$props"]; -export default FormModal; diff --git a/src/components/form/form-node.tsx b/src/components/form/form-node.tsx deleted file mode 100644 index b4d39c4..0000000 --- a/src/components/form/form-node.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { - AutoComplete, - Button, - Cascader, - CheckboxGroup, - DatePicker, - Input, - InputNumber, - InputPassword, - InputSearch, - RadioGroup, - RangePicker, - Select, - Slider, - Textarea, - TimePicker, - TreeSelect, -} from "@arco-design/web-vue"; -import { initOptions } from "./form-config"; - -/** - * 表单项组件映射 - */ -export const nodeMap = { - /** - * 输入框 - */ - input: { - component: Input, - nodeProps: { - placeholder: "请输入", - allowClear: true, - } as InstanceType["$props"], - }, - /** - * 搜索框 - */ - search: { - component: InputSearch, - nodeProps: { - placeholder: "请输入", - allowClear: true, - } as InstanceType["$props"] & InstanceType["$props"], - }, - /** - * 文本域 - */ - textarea: { - component: Textarea, - nodeProps: { - placeholder: "请输入", - allowClear: true, - } as InstanceType["$props"], - }, - /** - * 数值输入框 - */ - number: { - component: InputNumber, - nodeProps: { - placeholder: "请输入", - defaultValue: 0, - allowClear: true, - } as InstanceType["$props"], - }, - /** - * 密码输入框 - */ - password: { - component: InputPassword, - nodeProps: { - placeholder: "请输入", - } as InstanceType["$props"], - }, - /** - * 选择框 - */ - select: { - component: Select, - nodeProps: { - placeholder: "请选择", - allowClear: true, - allowSearch: true, - options: [{}], - } as InstanceType["$props"], - init: initOptions, - }, - /** - * 选择框 - */ - treeSelect: { - component: TreeSelect, - nodeProps: { - placeholder: "请选择", - allowClear: true, - allowSearch: true, - options: [], - onChange(value) { - value; - }, - } as InstanceType["$props"], - init: (arg: any) => initOptions(arg, "data"), - }, - /** - * 级联选择框 - */ - cascader: { - component: Cascader, - init: initOptions, - nodeProps: { - placeholder: "请选择", - allowClear: true, - expandTrigger: "hover", - } as InstanceType["$props"], - }, - /** - * 时间选择框 - */ - time: { - component: TimePicker, - nodeProps: { - allowClear: true, - } as InstanceType["$props"], - }, - /** - * 日期选择框 - */ - date: { - component: DatePicker, - nodeProps: { - allowClear: true, - } as InstanceType["$props"], - }, - /** - * 日期范围选择框 - */ - dateRange: { - component: RangePicker, - nodeProps: { - allowClear: true, - } as InstanceType["$props"], - }, - /** - * 复选框 - */ - checkbox: { - component: CheckboxGroup, - nodeProps: { - allowClear: true, - } as InstanceType["$props"], - init: initOptions, - }, - /** - * 复选框 - */ - radio: { - component: RadioGroup, - nodeProps: { - allowClear: true, - } as InstanceType["$props"], - init: initOptions, - }, - /** - * 滑动输入条 - */ - slider: { - component: Slider, - nodeProps: { - allowClear: true, - } as InstanceType["$props"], - }, - /** - * 自动完成输入框 - */ - autoComplete: { - component: AutoComplete, - nodeProps: { - allowClear: true, - } as InstanceType["$props"], - }, - /** - * 底部 - */ - submit: { - component: (props: any, { emit }: any) => { - return ( - <> - - {/* */} - - ); - }, - nodeProps: {}, - }, - /** - * 自定义组件 - */ - custom: { - nodeProps: {}, - component: () => null, - }, -}; - -/** - * 所有组件类型 - */ -export type NodeMap = typeof nodeMap; - -/** - * 组件类型 - */ -export type NodeType = keyof NodeMap; - -/** - * 提供给`FormItem`的联合类型 - * @description 当输入type,nodeProps会提供对应类型提示 - */ -export type NodeUnion = { - [key in NodeType]: { - /** - * 输入框类型,默认为`input` - */ - type: key; - /** - * 传递给`type`属性对应组件的参数 - */ - nodeProps?: NodeMap[key]["nodeProps"]; - }; -}[NodeType]; \ No newline at end of file diff --git a/src/components/form/form-rules.ts b/src/components/form/form-rules.ts deleted file mode 100644 index 4fec365..0000000 --- a/src/components/form/form-rules.ts +++ /dev/null @@ -1,46 +0,0 @@ -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: "至少包含大写字母、小写字母、数字和特殊字符", - }, -}); \ No newline at end of file diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx deleted file mode 100644 index 49b8fac..0000000 --- a/src/components/form/form.tsx +++ /dev/null @@ -1,120 +0,0 @@ -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"; - -type SubmitFn = (arg: { model: Record; items: IFormItem[] }) => Promise; - -/** - * 表单组件 - */ -export const Form = defineComponent({ - name: "Form", - props: { - /** - * 表单数据 - */ - model: { - type: Object as PropType>, - default: () => reactive({}), - }, - /** - * 表单项 - */ - items: { - type: Array as PropType, - default: () => [], - }, - /** - * 提交表单 - */ - submit: { - type: Function as PropType, - }, - /** - * 传给Form组件的参数 - */ - formProps: { - type: Object as PropType>, - }, - }, - setup(props) { - const model = cloneDeep(props.model); - const formRef = ref>(); - const loading = ref(false); - - props.items.forEach((item: any) => { - const node = nodeMap[item.type as NodeType]; - defaultsDeep(item, { nodeProps: node?.nodeProps ?? {} }); - (node as any)?.init?.({ item, model: props.model }); - }); - - const getItem = (field: string) => { - return props.items.find((item) => item.field === field); - }; - - const getModel = () => { - return config.getModel(props.model); - }; - - const setModel = (data: Record) => { - config.setModel(props.model, data); - }; - - const resetModel = () => { - assign(props.model, model); - }; - - const submitForm = async () => { - if (await formRef.value?.validate()) { - return; - } - const model: Record = getModel(); - try { - loading.value = true; - const res = await props.submit?.({ model, items: props.items }); - res?.message && Message.success(`提示: ${res.message}`); - } catch (error: any) { - const message = error?.response?.data?.message || error?.message; - message && Message.error(`提示: ${message}`); - } finally { - loading.value = false; - } - }; - - return { - formRef, - loading, - getItem, - submitForm, - resetModel, - setModel, - getModel, - }; - }, - render() { - (this.items as any).instance = this; - - const props = { - items: this.items, - model: this.model, - slots: this.$slots, - }; - - return ( - - {this.items.map((item) => ( - - ))} - - ); - }, -}); - -export type FormInstance = InstanceType; - -export type FormProps = FormInstance["$props"]; - -export type FormDefinedProps = Pick; \ No newline at end of file diff --git a/src/components/form/index.ts b/src/components/form/index.ts deleted file mode 100644 index e78507b..0000000 --- a/src/components/form/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./form"; -export * from "./use-form"; -export * from "./form-modal"; -export * from "./use-form-modal"; diff --git a/src/components/form/use-form-modal.tsx b/src/components/form/use-form-modal.tsx deleted file mode 100644 index 7380242..0000000 --- a/src/components/form/use-form-modal.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Modal } from "@arco-design/web-vue"; -import { merge } from "lodash-es"; -import { Component, Ref, reactive } from "vue"; -import { useForm } from "./use-form"; -import FormModal, { FormModalInstance, FormModalProps } from "./form-modal"; - -const defaults: Partial> = { - width: 1080, - titleAlign: "start", - closable: false, - maskClosable: false, -}; - -/** - * 构建传给FormModal组件的参数 - * @see src/components/form/use-form-modal.tsx - */ -export const useFormModal = (options: Partial): FormModalProps => { - const { model = {}, items = [] } = options || {}; - const form = useForm({ model, items }); - - return reactive( - merge( - { - modalProps: { ...defaults }, - formProps: { - layout: "vertical", - }, - }, - { - ...options, - ...form, - } - ) - ); -}; - -interface Context { - props: any; - modalRef: Ref; - open: (args?: Record) => Promise | undefined; -} - -export const useAniFormModal = (options: Partial): [Component, Context] => { - const props = useFormModal(options); - const modalRef = ref(null); - const onModalRef = (el: any) => (modalRef.value = el); - const component = defineComponent({ - name: "AniFormModalWrapper", - render() { - return ; - }, - }); - const component1 = (p: any) => ; - const context = { - props, - modalRef, - open: (args?: Record) => modalRef.value?.open(args), - }; - return [component1, context]; -}; diff --git a/src/components/form/use-form.tsx b/src/components/form/use-form.tsx deleted file mode 100644 index 3a18c86..0000000 --- a/src/components/form/use-form.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { FormInstance } from "@arco-design/web-vue"; -import { merge } from "lodash-es"; -import { IFormItem } from "./form-item"; - -export type Options = { - /** - * 表单数据模型 - */ - model?: Record; - /** - * 表单项数组 - */ - items: IFormItem[]; - /** - * 提交表单 - */ - submit?: (arg: { model: Record; items: IFormItem[] }) => Promise; - /** - * 表单实例属性 - */ - formProps?: Partial; -}; - -/** - * 构建表单组件的参数 - * @see src/components/form/use-form.tsx - */ -export const useForm = (options: Options) => { - const { model: _model = {} } = options; - const model: Record = { id: undefined, ..._model }; - const items: IFormItem[] = []; - - for (const item of options.items) { - if (!item.nodeProps) { - item.nodeProps = {} as any; - } - model[item.field] = model[item.field] ?? item.initial; - items.push(item); - } - - if (options.submit) { - const submit = items.find((item) => item.type === "submit") || {}; - items.push( - merge( - {}, - { - field: "id", - type: "submit", - itemProps: { - hideLabel: true, - }, - }, - submit - ) as any - ); - } - - return reactive({ ...options, model, items }) as any; -}; \ No newline at end of file diff --git a/src/components/form/util.ts b/src/components/form/util.ts deleted file mode 100644 index dd08554..0000000 --- a/src/components/form/util.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { cloneDeep } from "lodash-es"; - -/** - * 获取表单数据 - */ -export function getModel(model: any) { - const data: Record = {}; - for (const key of Object.keys(model)) { - if (/[^:]+:[^:]+/.test(key)) { - const keys = key.split(":"); - const vals = cloneDeep(model[key] || []); - for (const k of keys) { - data[k] = vals.shift(); - } - } else { - data[key] = cloneDeep(model[key]); - } - } - return data; -} - -/** - * 设置表单数据 - */ -export function setModel(model: any, data: Record) { - for (const key of Object.keys(model)) { - if (/[^:]+:[^:]+/.test(key)) { - const subKeys = key.split(":"); - model[key] = subKeys.map((k) => data[k]); - } else { - model[key] = data[key]; - } - } -} \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts deleted file mode 100644 index 22aa00a..0000000 --- a/src/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './form'; -export * from './table'; \ No newline at end of file diff --git a/src/components/table/README.md b/src/components/table/README.md deleted file mode 100644 index 4afa42c..0000000 --- a/src/components/table/README.md +++ /dev/null @@ -1,162 +0,0 @@ -### 基本用法 - -```typescript - - -``` - -以上,就是一个 CRUD 表格的简单用法。参数描述: -| 参数 | 说明 | 类型 | -| :--- | :--- | :--- | -| data | 表格数据,可为数组或函数(发起 HTTP 请求) | BaseData[] | ((search, paging) => Promise) | -| columns | 表格列,参见 [TableColumnData](https://arco.design/vue/component/table#TableColumnData) 文档,增加和扩展部分属性,详见下文。 | TableColumnData[] | -| pagination | 分页参数,参见 [Pagination](https://arco.design/vue/component/pagination) 文档,默认 15/每页。| Pagination | -| search | 搜索表单的配置,参见 [Form]() 说明,其中 `submit` 参数不可用 | FormProps | -| common | 新增和修改表单弹窗的公用参数,参见 [FormModal]() 说明。 | FormModalProps | -| create | 新增表单弹窗的参数,参见 [FormModal]() 说明, 将与`common`参数合并。 | FormModalProps | -| modify | 修改表单弹窗的参数,参见 [FormModal]() 说明, 将与`common`参数合并。 | FormModalProps | -| tableProps | 传递给`Table`组件的参数,参见 [Table](https://arco.design/vue/component/table) 文档,其中`columns`参数不可用。| TableProps | - -### 表格数据 - -`data`定义表格数据,可以是数组或函数。 - -- 当是数组时,直接用作数据源。 -- 当是函数时,传入查询参数和分页参数,可返回数组或对象,返回数组作用同上,返回对象时需遵循`{ data: [], total: number }`格式,用于分页处理。 - -用法示例: - -```typescript -const table = useTable({ - data: async (search, paging) { - const res = await api.xx({ ...search, ...paging }); - return { - data: res.data, - total: res.total - } - } -}) -``` - -### 表格列 - -`columns`定义表格列,并在原本基础上增加默认值并扩展部分属性。增加和扩展的属性如下: - -| 参数 | 说明 | 类型 | -| :------ | :--------------------------------------------------------------------------------------------------- | :------- | -------- | -| type | 特殊类型, 目前支持`index`(表示行数)、`button`(行操作按钮) | 'index' | 'button' | -| buttons | 当`type`为`button`时的按钮数组,如果子项是对象则为`Button`组件的参数,如果为函数则为自定义渲染函数。 | Button[] | - -### 表格分页 - -`pagination`定义分页行为,具体参数可参考 [Pagination](https://arco.design/vue/component/pagination) 文档。当`data`为数组时,将作为数据源进行分页;当`data`为函数且返回值为对象时,则根据`total`值进行分页。 - -### 搜索表单 - -参阅 - -### 公共参数 - -参数为`FormModal`的参数,主要作为新增和修改的公共参数。在大多数情况,新增和修改的配置大多是相似的,没必要写两份,把相同的参数写在这里即可,不同的参数在`create`和`modify`中单独配置。 - -注意,这里的`items`也可以被搜索表单复用,搜索表单可通过`extends: `继承`common.items`中对应的字段配置。使用示例如下: - -```typescript -const table = useTable({ - common: { - items: [ - { - field: "username", - label: "用户名称", - type: "input", - required: true, - }, - ], - }, - search: { - items: [ - { - extend: "usernam", - required: false, - }, - ], - }, -}); -``` - -### 新增弹窗 - -`create`为新增表单弹窗的参数,即`useFormModal`对应的参数。参阅。当指定该参数时,会在表格左上添加新建按钮,如需自定义按钮样式或自定义渲染,可通过`create.trigger`参数配置。 - -### 修改弹窗 - -`modify`为新增表单弹窗的参数,即`useFormModal`对应的参数。参阅。当指定该参数时,会在表格行添加修改按钮。 - -### 表格参数 - -`tableProps`为传递给`Table`组件的额外参数,其中部分参数不可用,如`data`和`columns`等。此外,部分参数有默认值,具体参数可查看`src/components/table/table.config.ts`文件。 - -### 插槽 - -- `Table`组件的插槽可正常使用 -- `action`插槽用作表格左上方的操作区。 - -## 问题 - -- 问题:日期范围框值为数组,处理不方便 -- 解决:字段名使用`v1:v2`格式,提交时会生成`{ v1: '00:00:01', v2: '00:00:02' }`数据 -- 问题:搜索表单、新增表单和修改表单通常用到同一表单项,如何避免重复定义 -- 解决:表单项使用`{ extends: }`会在`common.items`中寻找相同的项,并合并值。 diff --git a/src/components/table/colume.tsx b/src/components/table/colume.tsx deleted file mode 100644 index f792292..0000000 --- a/src/components/table/colume.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { dayjs } from "@/libs/dayjs"; -import { TableColumn } from "./use-interface"; - -const defineColumn = (column: T) => { - return column; -}; - -export const updateColumn = defineColumn({ - title: "更新用户", - dataIndex: "createdAt", - width: 190, - render({ record }) { - return ( -
- {record.updatedBy ?? "无"} - - {dayjs(record.updatedAt).format()} - -
- ); - }, -}); - -export const createColumn = defineColumn({ - title: "创建用户", - dataIndex: "createdAt", - width: 190, - render({ record }) { - return ( -
- {record.createdBy ?? "无"} - - {dayjs(record.createdAt).format()} - -
- ); - }, -}); diff --git a/src/components/table/index.ts b/src/components/table/index.ts deleted file mode 100644 index 8807de4..0000000 --- a/src/components/table/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./colume"; -export * from "./table"; -export * from "./use-table"; - diff --git a/src/components/table/table.config.tsx b/src/components/table/table.config.tsx deleted file mode 100644 index 6e3bc85..0000000 --- a/src/components/table/table.config.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Button } from "@arco-design/web-vue"; -import { IconRefresh, IconSearch } from "@arco-design/web-vue/es/icon"; - -export const config = { - searchInlineCount: 3, - searchFormProps: { - labelAlign: "left", - autoLabelWidth: true, - model: {}, - }, - searchItemSubmit: { - field: "id", - type: "custom", - label: ' ', - 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) > config.searchInlineCount && ( - - )} - -
- ); - }, - }, - 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) => { - const table = inject("ref:table"); - const page = table.pagination.current; - const size = table.pagination.pageSize; - return size * (page - 1) + rowIndex + 1; - }, - }, - columnButtonBase: { - buttonProps: { - // type: "text", - // size: "mini", - }, - }, - columnButtonDelete: { - title: "删除确认", - content: "确认删除当前数据吗?", - modalClass: "text-center", - hideCancel: false, - maskClosable: false, - }, - columnDropdownModify: { - text: "修改", - icon: "icon-park-outline-edit", - }, - getApiErrorMessage(error: any): string { - const message = error?.response?.data?.message || error?.message || "请求失败"; - return ''; - }, -}; diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx deleted file mode 100644 index 8c64fb3..0000000 --- a/src/components/table/table.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import AniEmpty from "@/components/AnEmpty/AnEmpty.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 { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form"; -import { config } from "./table.config"; - -type DataFn = (search: Record, paging: { page: number; size: number }) => Promise; - -/** - * 表格组件 - * @see src/components/table/table.tsx - */ -export const Table = defineComponent({ - name: "Table", - props: { - /** - * 表格数据 - * @description 可以是数组或者函数,函数返回值为`{ data: BaseData[], total: number }`类型 - */ - data: { - type: [Array, Function] as PropType, - }, - /** - * 表格列设置 - */ - columns: { - type: Array as PropType, - default: () => [], - }, - /** - * 分页参数配置 - */ - pagination: { - type: Object as PropType, - default: () => reactive(config.pagination), - }, - /** - * 搜索表单配置 - */ - search: { - type: Object as PropType, - }, - /** - * 新建弹窗配置 - */ - create: { - type: Object as PropType, - }, - /** - * 修改弹窗配置 - */ - modify: { - type: Object as PropType, - }, - /** - * 详情弹窗配置 - */ - detail: { - type: Object as PropType, - }, - /** - * 传递给 Table 组件的属性 - */ - tableProps: { - type: Object as PropType["$props"]>, - }, - }, - setup(props) { - const loading = ref(false); - const tableRef = ref>(); - const searchRef = ref(); - const createRef = ref(); - const modifyRef = ref(); - const renderData = ref([]); - const inlined = computed(() => (props.search?.items?.length ?? 0) <= config.searchInlineCount); - const reloadData = () => loadData({ current: 1, pageSize: 10 }); - const openModifyModal = (data: any) => modifyRef.value?.open(data); - - /** - * 加载数据 - * @param pagination 自定义分页 - */ - const loadData = async (pagination: Partial = {}) => { - const merged = { ...props.pagination, ...pagination }; - const paging = { page: merged.current, size: merged.pageSize }; - const model = searchRef.value?.getModel() ?? {}; - - // 本地加载 - if (Array.isArray(props.data)) { - 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); - } - return item[key] === value; - }); - }); - renderData.value = data; - props.pagination.total = renderData.value.length; - props.pagination.current = 1; - } - - // 远程加载 - if (typeof props.data === "function") { - try { - loading.value = true; - const resData = await props.data(model, paging); - const { data = [], total = 0 } = resData?.data || {}; - renderData.value = data; - props.pagination.total = total; - props.pagination.current = paging.page; - } catch (e) { - // todo - } finally { - loading.value = false; - } - } - }; - - watchEffect(() => { - if (Array.isArray(props.data)) { - renderData.value = props.data; - props.pagination.total = props.data.length; - props.pagination.current = 1; - } - }); - - onMounted(() => { - loadData(); - }); - - if (props.search) { - merge(props.search, { formProps: { layout: "inline" } }); - } - - const state = { - loading, - inlined, - tableRef, - searchRef, - createRef, - modifyRef, - renderData, - loadData, - reloadData, - openModifyModal, - }; - - provide("ref:table", { ...state, ...props }); - - return state; - }, - render() { - (this.columns as any).instance = this; - return ( -
- {!this.inlined && ( -
-
-
- )} - -
-
- {this.create && ( - - )} - {this.modify && ( - - )} - {this.$slots.action?.()} -
-
{this.inlined &&
}
-
- - this.loadData({ current })} - > - {{ - empty: () => , - ...this.$slots, - }} - -
- ); - }, -}); - -/** - * 表格组件实例 - */ -export type TableInstance = InstanceType; - -/** - * 表格组件参数 - */ -export type TableProps = TableInstance["$props"]; \ No newline at end of file diff --git a/src/components/table/use-interface.ts b/src/components/table/use-interface.ts deleted file mode 100644 index 1f087dc..0000000 --- a/src/components/table/use-interface.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Doption, Link, TableColumnData, TableData } from "@arco-design/web-vue"; -import { RenderFunction } from "vue"; -import { FormModalProps, FormProps } from "../form"; -import { IFormItem } from "../form/form-item"; -import { TableProps } from "./table"; - -interface UseColumnRenderOptions { - /** - * 当前行数据 - */ - record: TableData; - /** - * 当前列配置 - */ - column: TableColumnData; - /** - * 当前行索引 - */ - rowIndex: number; -} - -export type ColumnRender = (options: UseColumnRenderOptions) => any; - -export interface TableColumnButton { - /** - * 按钮文本 - */ - text?: string; - /** - * 操作类型 - * @description `delete` 需配置`onClick`属性,`modify` 需配置根对象下的 `modify` 属性 - */ - type?: "delete" | "modify"; - /** - * 处理函数 - */ - onClick?: (data: UseColumnRenderOptions) => any; - /** - * 是否禁用按钮 - */ - disabled?: (data: UseColumnRenderOptions) => boolean; - /** - * 是否显示按钮 - */ - visible?: (data: UseColumnRenderOptions) => boolean; - /** - * 传递给按钮的props - */ - buttonProps?: Partial["$props"], "onClick" | "disabled">>; -} - -interface TableColumnDropdown { - /** - * 特殊类型 - */ - type?: "modify" | "delete"; - /** - * 下拉菜单文本 - */ - text?: string; - /** - * 下拉菜单图标 - */ - icon?: string | RenderFunction; - /** - * 是否禁用 - */ - disabled?: (data: UseColumnRenderOptions) => boolean; - /** - * 是否显示 - */ - visibled?: (data: UseColumnRenderOptions) => boolean; - /** - * 处理事件 - */ - onClick?: (data: UseColumnRenderOptions) => any; - /** - * - */ - doptionProps?: Partial & Record>; -} - -export interface TableColumn extends TableColumnData { - /** - * 表格列类型 - */ - type?: "index" | "button" | "dropdown"; - /** - * 按钮配置列表 - */ - buttons?: TableColumnButton[]; - /** - * 下拉菜单配置列表 - */ - dropdowns?: TableColumnDropdown[]; -} - -type ExtendedFormItem = Partial & { - /** - * 继承 `create.items` 中指定 `field` 值的项 - */ - extend?: string; -}; - -type SearchFormItem = ExtendedFormItem & { - enableLoad?: boolean; - searchable?: boolean; - enterable?: boolean; -}; - -type Search = Partial< - Omit & { - /** - * 表单项 - */ - items?: SearchFormItem[]; - /** - * 显示/隐藏搜索按钮 - */ - button?: boolean; - } ->; - -type Modify = Partial< - Omit & { - /** - * 是否继承 `create` 弹窗配置 - */ - extend: boolean; - /** - * 表单项 - */ - items?: ExtendedFormItem[]; - } ->; - -export interface UseTableOptions extends Omit { - /** - * 表格列配置 - * @see https://arco.design/web-vue/components/table/#tablecolumn - */ - columns: TableColumn[]; - /** - * 搜索表单配置 - * @see FormProps - */ - search?: Search; - /** - * 新建弹窗配置 - */ - create?: Partial; - /** - * 新建弹窗配置 - */ - modify?: Modify; - /** - * 详情弹窗配置 - */ - detail?: any; - /** - * 批量删除配置 - */ - delete?: any; -} diff --git a/src/components/table/use-table.tsx b/src/components/table/use-table.tsx deleted file mode 100644 index 6629970..0000000 --- a/src/components/table/use-table.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { delConfirm } from "@/utils"; -import { Divider, Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue"; -import { isArray, merge } from "lodash-es"; -import { Component, Ref, reactive } from "vue"; -import { useFormModal } from "../form"; -import { Table, TableInstance, TableProps } from "./table"; -import { config } from "./table.config"; -import { UseTableOptions } from "./use-interface"; - -const onClick = async (item: any, columnData: any, getTable: any) => { - if (item.type === "modify") { - const data = (await item.onClick?.(columnData)) ?? columnData.record; - getTable()?.openModifyModal(data); - return; - } - if (item.type === "delete") { - await delConfirm(); - try { - const resData: any = await item?.onClick?.(columnData); - const message = resData?.data?.message; - message && Message.success(`提示:${message}`); - getTable()?.loadData(); - } catch (error: any) { - const message = error.response?.data?.message; - message && Message.warning(`提示:${message}`); - } - return; - } - item.onClick?.(columnData); -}; - -/** - * 表格组件hook - * @see `src/components/table/use-table.tsx` - */ -export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)): any => { - const options: UseTableOptions = typeof optionsOrFn === "function" ? optionsOrFn() : optionsOrFn; - const columns: TableColumnData[] = []; - const getTable = (): TableInstance => (columns as any).instance; - - /** - * 表格列处理 - */ - for (let column of options.columns) { - /** - * 索引列处理 - */ - if (column.type === "index") { - column = merge({}, config.columnIndex, column); - } - - /** - * 按钮列处理 - */ - if (column.type === "button" && isArray(column.buttons)) { - const buttons = column.buttons; - let hasModify = false; - let hasDelete = false; - for (let i = 0; i < buttons.length; i++) { - let btn = merge({}, config.columnButtonBase); - if (buttons[i].type === "modify") { - btn = merge(btn, buttons[i]); - hasModify = true; - } - if (buttons[i].type === "delete") { - btn = merge(btn, buttons[i]); - hasDelete = true; - } - buttons[i] = merge(btn, buttons[i]); - } - if (!hasModify) { - buttons.push(merge({}, config.columnButtonBase)); - } - if (!hasDelete) { - buttons.push(merge({}, config.columnButtonBase)); - } - column.render = (columnData) => { - return column.buttons?.map((btn, index) => { - if (btn.visible?.(columnData) === false) { - return null; - } - return ( - <> - {index !== 0 ? : null} - onClick(btn, columnData, getTable)} - disabled={btn.disabled?.(columnData)} - > - {btn.text} - - - ); - }); - }; - } - - /** - * 菜单列处理 - */ - if (column.type === "dropdown" && Array.isArray(column.dropdowns)) { - if (options.modify) { - const index = column.dropdowns?.findIndex((i) => i.type === "modify"); - if (index !== undefined) { - column.dropdowns[index] = merge({}, config.columnDropdownModify, column.dropdowns[index]); - } else { - column.dropdowns?.unshift(merge({}, config.columnDropdownModify)); - } - } - column.render = (columnData) => { - const content = column.dropdowns?.map((dropdown) => { - const { text, icon, disabled, visibled, doptionProps } = dropdown; - if (visibled?.(columnData) === false) { - return null; - } - return ( - onClick(dropdown, columnData, getTable)} - disabled={disabled?.(columnData)} - > - {{ - icon: typeof icon === "function" ? icon() : () => , - default: text, - }} - - ); - }); - const trigger = () => ( - - - - ); - return ( - <> - 编辑 - 详情 - - {{ - default: trigger, - content: content, - }} - - - ); - }; - } - - columns.push({ ...config.columnBase, ...column }); - } - - /** - * 新增表单处理 - */ - if (options.create) { - options.create = useFormModal(options.create as any) as any; - } - - /** - * 搜索表单的处理 - */ - if (options.search && options.search.items) { - const searchItems: any[] = []; - const createItems = options.create?.items ?? []; - for (const item of options.search.items) { - if (item.extend) { - const createItem = createItems.find((i) => i.field === item.extend); - if (createItem) { - searchItems.push(merge({}, createItem, item)); - continue; - } - } - const onSearch = item.searchable ? () => getTable().reloadData() : undefined; - const onPressEnter = item.enterable ? () => getTable().reloadData() : undefined; - searchItems.push(merge({ nodeProps: { onSearch, onPressEnter } }, item)); - } - if (options.search.button !== false) { - searchItems.push(config.searchItemSubmit); - } - options.search.items = searchItems; - } - - /** - * 修改表单处理 - */ - if (options.modify) { - if (options.modify.extend && options.create) { - const createItems = options.create.items; - const modifyItems = options.modify.items; - if (modifyItems && createItems) { - for (let i = 0; i < modifyItems.length; i++) { - if (modifyItems[i].extend) { - modifyItems[i] = merge({}, createItems[i], modifyItems[i]); - } - } - } - const merged = merge( - { modalProps: { titleAlign: "start", closable: false }, model: { id: undefined } }, - options.create, - options.modify - ); - options.modify = useFormModal(merged as any) as any; - } else { - options.modify = useFormModal(options.modify as any) as any; - } - } - - return reactive({ ...options, columns }); -}; - -/** - * 提供操作的上下文 - */ -interface TableContext { - /** - * 传递给表格的参数(响应式) - */ - props: TableProps; - /** - * 表格实例 - */ - tableRef: Ref; - /** - * 刷新表格 - */ - refresh: () => void; - /** - * 重置表格 - */ - reload?: () => void; -} - -type TableReturnType = [ - /** - * 绑定好参数的组件 - */ - Component, - /** - * 提供操作的上下文 - */ - TableContext -]; - -export const useAniTable = (options: UseTableOptions): TableReturnType => { - const props = useTable(options); - const tableRef = ref(null); - const context = { - props, - tableRef, - refresh: () => tableRef.value?.reloadData(), - getTableInstance() { - return tableRef.value?.tableRef; - }, - getSearchInstance() { - return tableRef.value?.searchRef; - }, - getCreateInstance() { - return tableRef.value?.createRef; - }, - /** - * 获取创建表单组件实例 - */ - getCreateFormInstance() { - return this.getCreateInstance()?.formRef; - }, - /** - * 获取修改表单弹窗组件实例 - */ - getModifyInstance() { - return tableRef.value?.modifyRef; - }, - /** - * 获取修改表单组件实例 - */ - getModifyFormInstance() { - return this.getModifyInstance()?.formRef; - }, - }; - const aniTable = defineComponent({ - name: "AniTableWrapper", - setup(p, { slots }) { - const onRef = (el: TableInstance) => (tableRef.value = el); - return () => {slots}
; - }, - }); - return [aniTable, context]; -}; diff --git a/src/pages/_layout/components/userDropdown.vue b/src/pages/_layout/components/userDropdown.vue index 23c731d..ff7911f 100644 --- a/src/pages/_layout/components/userDropdown.vue +++ b/src/pages/_layout/components/userDropdown.vue @@ -23,7 +23,7 @@ - + @@ -47,51 +47,52 @@ diff --git a/src/pages/content/media/components/AnGroup.vue b/src/pages/content/media/components/AnGroup.vue index 73864b8..9dfdc7f 100644 --- a/src/pages/content/media/components/AnGroup.vue +++ b/src/pages/content/media/components/AnGroup.vue @@ -2,12 +2,12 @@
- + - +
@@ -30,7 +30,7 @@