diff --git a/.editorconfig b/.editorconfig index db1d3f8a2..284364ffa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,4 @@ -# 🎨 editorconfig.org - -root = true - -[*] +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] charset = utf-8 end_of_line = lf indent_style = tab diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 3c018eed2..000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -vite.config.ts \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 41fa72b82..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,56 +0,0 @@ -module.exports = { - root: true, - env: { - browser: true, - node: true, - es6: true - }, - parser: "vue-eslint-parser", - parserOptions: { - parser: "@typescript-eslint/parser", - ecmaVersion: 2020, - sourceType: "module", - jsxPragma: "React", - ecmaFeatures: { - jsx: true, - tsx: true - } - }, - extends: [ - "plugin:vue/vue3-recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - "plugin:prettier/recommended" - ], - rules: { - "@typescript-eslint/ban-ts-ignore": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-empty-function": "off", - "vue/no-mutating-props": "off", - "vue/component-name-in-template-casing": ["error", "kebab-case"], - "vue/component-definition-name-casing": ["error", "kebab-case"], - "no-use-before-define": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-namespace": "off", - "@typescript-eslint/no-unused-vars": "off", - "space-before-function-paren": "off", - "vue/attributes-order": "off", - "vue/one-component-per-file": "off", - "vue/html-closing-bracket-newline": "off", - "vue/max-attributes-per-line": "off", - "vue/multiline-html-element-content-newline": "off", - "vue/multi-word-component-names": "off", - "vue/singleline-html-element-content-newline": "off", - "vue/attribute-hyphenation": "off", - "vue/html-self-closing": "off", - "vue/require-default-prop": "off", - "vue/v-on-event-hyphenation": "off" - } -}; diff --git a/.gitignore b/.gitignore index 4816e5a0a..df6798f08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store node_modules/ /dist/ +/build/ dist-ssr/ # Log files diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index ee44e1103..000000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "tabWidth": 4, - "useTabs": true, - "semi": true, - "singleQuote": false, - "printWidth": 100, - "trailingComma": "none" -} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..f7d2f5cb7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": true, + "useTabs": true, + "tabWidth": 4, + "printWidth": 100, + "singleQuote": true, + "arrowParens": "avoid", + "trailingComma": "none" +} diff --git a/.vscode/crud.code-snippets b/.vscode/crud.code-snippets index 01b3dde79..1a665b5ca 100644 --- a/.vscode/crud.code-snippets +++ b/.vscode/crud.code-snippets @@ -33,7 +33,7 @@ " ", "", "", - " diff --git a/packages/crud/src/components/adv/search.tsx b/packages/crud/src/components/adv/search.tsx index 8b4e5922e..b9c7d9f7c 100644 --- a/packages/crud/src/components/adv/search.tsx +++ b/packages/crud/src/components/adv/search.tsx @@ -3,6 +3,7 @@ import { Close } from "@element-plus/icons-vue"; import { useBrowser, useConfig, useCore } from "../../hooks"; import { renderNode } from "../../utils/vnode"; import { useApi } from "../form/helper"; +import { isArray } from "lodash-es"; export default defineComponent({ name: "cl-adv-search", @@ -58,7 +59,7 @@ export default defineComponent({ function open() { visible.value = true; - nextTick(function () { + nextTick(() => { Form.value?.open({ items: config.items || [], op: { @@ -76,9 +77,30 @@ export default defineComponent({ // 重置数据 function reset() { + const d: any = {}; + + config.items?.map((e) => { + if (typeof e.hook != 'string' && e.hook?.reset) { + const props = e.hook.reset(e.prop!) + + if (isArray(props)) { + props.forEach((prop) => { + d[prop] = undefined; + }) + } + } + + d[e.prop!] = undefined; + }); + + // 重置表单 Form.value?.reset(); - emit("reset"); + + // 列表刷新 search(); + + // 重置事件 + emit("reset", d); } // 清空数据 @@ -88,24 +110,25 @@ export default defineComponent({ } // 搜素请求 - function search() { - Form.value?.submit((data) => { - function next(params: any) { - Form.value?.done(); - close(); - - return crud.refresh({ - ...params, - page: 1 - }); - } + function search(params?: any) { + const form = Form.value?.getForm(); - if (config.onSearch) { - config.onSearch(data, { next, close }); - } else { - next(data); - } - }); + function next(data: any) { + Form.value?.done(); + close(); + + return crud.refresh({ + ...data, + ...params, + page: 1 + }); + } + + if (config.onSearch) { + config.onSearch(form, { next, close }); + } else { + next(form); + } } // 消息事件 @@ -131,7 +154,9 @@ export default defineComponent({ { type: e == "search" ? "primary" : null, size: style.size, - onClick: fns[e] + onClick: () => { + fns[e](); + } }, { default: () => crud.dict.label[e] } ); @@ -149,8 +174,9 @@ export default defineComponent({ open, close, clear, + ...useApi({ Form }), reset, - ...useApi({ Form }) + Form }); return () => { diff --git a/packages/crud/src/components/context-menu/index.tsx b/packages/crud/src/components/context-menu/index.tsx index 7d1333f84..66bf85b14 100644 --- a/packages/crud/src/components/context-menu/index.tsx +++ b/packages/crud/src/components/context-menu/index.tsx @@ -1,4 +1,14 @@ -import { defineComponent, nextTick, onMounted, reactive, ref, h, render, toRaw } from "vue"; +import { + defineComponent, + nextTick, + onMounted, + reactive, + ref, + h, + render, + toRaw, + type PropType +} from "vue"; import { isString } from "lodash-es"; import { addClass, contains, removeClass } from "../../utils"; import { useRefs } from "../../hooks"; @@ -11,16 +21,12 @@ const ClContextMenu = defineComponent({ props: { show: Boolean, options: { - type: Object, - default: () => { - return {}; - } + type: Object as PropType, + default: () => ({}) }, event: { type: Object, - default: () => { - return {}; - } + default: () => ({}) } }, @@ -43,7 +49,7 @@ const ClContextMenu = defineComponent({ const ids = ref(""); // 阻止默认事件 - function stopDefault(e: MouseEvent) { + function stopDefault(e: any) { if (e.preventDefault) { e.preventDefault(); } @@ -71,26 +77,31 @@ const ClContextMenu = defineComponent({ } // 目标元素 - let targetEl: any = null; + let targetEl: any; // 关闭 function close() { visible.value = false; ids.value = ""; - removeClass(targetEl, "cl-context-menu__target"); + + if (targetEl) { + removeClass(targetEl, "cl-context-menu__target"); + } } // 打开 - function open(event: any, options?: any) { - let left = event.pageX; - let top = event.pageY; + function open(event: any, options: ClContextMenu.Options = {}) { + // 阻止默认事件 + stopDefault(event); - if (!options) { - options = {}; - } + // 显示 + visible.value = true; + + // 元素 + const el = refs["context-menu"].querySelector(".cl-context-menu__box") as HTMLElement; // 点击样式 - if (options.hover) { + if (options?.hover) { const d = options.hover === true ? {} : options.hover; targetEl = event.target; @@ -105,20 +116,29 @@ const ClContextMenu = defineComponent({ } } - if (options.list) { - list.value = parseList(options.list); + // 自定义样式 + if (options?.class) { + addClass(el, options.class); } - // 阻止默认事件 - stopDefault(event); - - // 显示 - visible.value = true; + // 菜单列表 + if (options?.list) { + list.value = parseList(options.list); + } nextTick(() => { - const { clientHeight: h1, clientWidth: w1 } = event.target.ownerDocument.body; - const { clientHeight: h2, clientWidth: w2 } = - refs["context-menu"].querySelector(".cl-context-menu__box"); + // 计算位置 + let left = event.pageX; + let top = event.pageY; + + // 组件方式用 offset 计算 + if (!props.show) { + left = event.offsetX; + top = event.offsetY; + } + + const { clientHeight: h1, clientWidth: w1 } = event.target?.ownerDocument.body; + const { clientHeight: h2, clientWidth: w2 } = el; if (top + h2 > h1) { top = h1 - h2 - 5; @@ -130,7 +150,7 @@ const ClContextMenu = defineComponent({ style.left = left + "px"; style.top = top + "px"; - }); + }) return { close @@ -176,7 +196,7 @@ const ClContextMenu = defineComponent({ }); // 默认打开 - open(props.event, props.options); + open(props.event, props?.options); } }); @@ -245,13 +265,15 @@ const ClContextMenu = defineComponent({ export const ContextMenu = { open(event: any, options: ClContextMenu.Options) { - const vm: any = h(ClContextMenu, { + const vm = h(ClContextMenu, { show: true, event, options }); render(vm, event.target.ownerDocument.createElement("div")); + + return vm.component?.exposed as ClContextMenu.Exposed; } }; diff --git a/packages/crud/src/components/crud/helper.ts b/packages/crud/src/components/crud/helper.ts index 4a3d916ad..ea2531606 100644 --- a/packages/crud/src/components/crud/helper.ts +++ b/packages/crud/src/components/crud/helper.ts @@ -1,7 +1,7 @@ import { ElMessageBox, ElMessage } from "element-plus"; import { Mitt } from "../../utils/mitt"; import { ref } from "vue"; -import { isArray, isFunction } from "lodash-es"; +import { assign, isArray, isFunction } from "lodash-es"; import { merge } from "../../utils"; interface Options { @@ -56,7 +56,7 @@ export function useHelper({ config, crud, mitt }: Options) { return new Promise((success, error) => { // 合并请求参数 - const reqParams = paramsReplace(Object.assign(crud.params, params)); + const reqParams = paramsReplace(assign(crud.params, params)); // Loading crud.loading = true; @@ -70,8 +70,8 @@ export function useHelper({ config, crud, mitt }: Options) { } // 渲染 - function render(list: any[], pagination?: any) { - const res = { list, pagination }; + function render(data: any | any[], pagination?: any) { + const res = isArray(data) ? { list: data, pagination } : data; done(); success(res); mitt.emit("crud.refresh", res); @@ -87,12 +87,15 @@ export function useHelper({ config, crud, mitt }: Options) { } if (isArray(res)) { - render(res); - } else { - render(res.list, res.pagination); + res = { + list: res, + pagination: { + total: res.length + } + }; } - success(res); + render(res); resolve(res); }) .catch((err) => { @@ -217,6 +220,11 @@ export function useHelper({ config, crud, mitt }: Options) { return crud.params; } + // 替换请求参数 + function setParams(data: obj) { + merge(crud.params, data); + } + // 设置 function set(key: string, value: any) { if (!value) { @@ -273,6 +281,7 @@ export function useHelper({ config, crud, mitt }: Options) { refresh, getPermission, paramsReplace, - getParams + getParams, + setParams }; } diff --git a/packages/crud/src/components/crud/index.tsx b/packages/crud/src/components/crud/index.tsx index affdbf7c5..097a1e37b 100644 --- a/packages/crud/src/components/crud/index.tsx +++ b/packages/crud/src/components/crud/index.tsx @@ -55,7 +55,9 @@ export default defineComponent({ // 字典 dict: {}, // 权限 - permission: {} + permission: {}, + // 事件 + mitt }, cloneDeep({ dict, permission }) ) diff --git a/packages/crud/src/components/form/helper/action.ts b/packages/crud/src/components/form/helper/action.ts index 9d85a0dca..eaf967466 100644 --- a/packages/crud/src/components/form/helper/action.ts +++ b/packages/crud/src/components/form/helper/action.ts @@ -1,3 +1,4 @@ +import { assign } from "lodash-es"; import { dataset } from "../../../utils"; export function useAction({ @@ -48,7 +49,7 @@ export function useAction({ break; case "props": - Object.assign(d.component.props, data); + assign(d.component.props, data); break; case "hidden": @@ -60,7 +61,7 @@ export function useAction({ break; default: - Object.assign(d, data); + assign(d, data); break; } } else { diff --git a/packages/crud/src/components/form/helper/api.ts b/packages/crud/src/components/form/helper/api.ts index 60d2168f1..83fb146fd 100644 --- a/packages/crud/src/components/form/helper/api.ts +++ b/packages/crud/src/components/form/helper/api.ts @@ -16,6 +16,7 @@ export function useApi({ Form }: { Form: Vue.Ref }) { "collapseItem", "getForm", "setForm", + "invokeData", "setData", "setConfig", "setOptions", diff --git a/packages/crud/src/components/form/helper/index.ts b/packages/crud/src/components/form/helper/index.ts index 7520fe502..f89d7ad3f 100644 --- a/packages/crud/src/components/form/helper/index.ts +++ b/packages/crud/src/components/form/helper/index.ts @@ -1,5 +1,6 @@ -import { reactive, ref } from "vue"; +import { reactive, ref, watch } from "vue"; import { useConfig } from "../../../hooks"; +import { cloneDeep } from "lodash-es"; export function useForm() { const { dict } = useConfig(); @@ -33,6 +34,9 @@ export function useForm() { // 表单数据 const form = reactive({}); + // 表单数据备份 + const oldForm = ref({}); + // 表单是否可见 const visible = ref(false); @@ -45,6 +49,25 @@ export function useForm() { // 表单禁用状态 const disabled = ref(false); + // 监听表单变化 + watch( + () => form, + (val) => { + if (config.on?.change) { + for (const i in val) { + if (form[i] !== oldForm.value[i]) { + config.on?.change(val, i); + } + } + } + + oldForm.value = cloneDeep(val); + }, + { + deep: true + } + ); + return { Form, config, diff --git a/packages/crud/src/components/form/index.tsx b/packages/crud/src/components/form/index.tsx index f810e57e9..c05ab116d 100644 --- a/packages/crud/src/components/form/index.tsx +++ b/packages/crud/src/components/form/index.tsx @@ -1,5 +1,5 @@ -import { defineComponent, h, nextTick } from "vue"; -import { cloneDeep, isBoolean } from "lodash-es"; +import { defineComponent, h, nextTick, toRef, watch } from "vue"; +import { assign, cloneDeep, isBoolean, keys } from "lodash-es"; import { useAction, useForm, usePlugins, useTabs } from "./helper"; import { useBrowser, useConfig, useElApi, useRefs } from "../../hooks"; import { getValue, merge } from "../../utils"; @@ -96,7 +96,7 @@ export default defineComponent({ // 清空表单验证 function clear() { for (const i in form) { - delete form[i]; + form[i] = undefined; } setTimeout(() => { @@ -113,6 +113,39 @@ export default defineComponent({ } } + // 转换表单值,处理多层级等数据 + function invokeData(d: any) { + for (const i in d) { + if (i.includes("-")) { + // 结构参数 + const [a, ...arr] = i.split("-"); + + // 关键值的key + const k: string = arr.pop() || ""; + + if (!d[a]) { + d[a] = {}; + } + + let f: any = d[a]; + + // 设置默认值 + arr.forEach((e) => { + if (!f[e]) { + f[e] = {}; + } + + f = f[e]; + }); + + // 设置关键值 + f[k] = d[i]; + + delete d[i]; + } + } + } + // 表单提交 function submit(callback?: fn) { // 验证表单 @@ -151,36 +184,8 @@ export default defineComponent({ deep(e); }); - // 处理 "-" 多层级 - for (const i in d) { - if (i.includes("-")) { - // 结构参数 - const [a, ...arr] = i.split("-"); - - // 关键值的key - const k: string = arr.pop() || ""; - - if (!d[a]) { - d[a] = {}; - } - - let f: any = d[a]; - - // 设置默认值 - arr.forEach((e) => { - if (!f[e]) { - f[e] = {}; - } - - f = f[e]; - }); - - // 设置关键值 - f[k] = d[i]; - - delete d[i]; - } - } + // 处理数据 + invokeData(d); const submit = callback || config.on?.submit; @@ -200,7 +205,7 @@ export default defineComponent({ Tabs.toGroup({ refs, config, - prop: Object.keys(error)[0] + prop: keys(error)[0] }); } }); @@ -342,7 +347,7 @@ export default defineComponent({ deep(e); }); - Object.assign(form, data); + assign(form, data); } // 渲染表单项 @@ -386,13 +391,15 @@ export default defineComponent({ e.props, { label() { - return e.renderLabel - ? renderNode(e.renderLabel, { - scope: form, - render: "slot", - slots - }) - : e.label; + if (e.renderLabel) { + return renderNode(e.renderLabel, { + scope: form, + render: "slot", + slots + }); + } else { + return e.label; + } }, default() { return ( @@ -568,9 +575,7 @@ export default defineComponent({ custom() { return ( { e.onClick({ scope: form }); @@ -609,6 +614,7 @@ export default defineComponent({ clear, reset, submit, + invokeData, bindForm, showLoading, hideLoading, diff --git a/packages/crud/src/components/index.tsx b/packages/crud/src/components/index.tsx index d5f38a014..b23c3446e 100644 --- a/packages/crud/src/components/index.tsx +++ b/packages/crud/src/components/index.tsx @@ -18,6 +18,7 @@ import Filter from "./filter"; import Search from "./search"; import ErrorMessage from "./error-message"; import Row from "./row"; +import ContextMenu from "./context-menu"; export const components: { [key: string]: any } = { Crud, @@ -38,7 +39,8 @@ export const components: { [key: string]: any } = { Filter, Search, ErrorMessage, - Row + Row, + ContextMenu }; export function useComponent(app: App) { diff --git a/packages/crud/src/components/pagination/index.tsx b/packages/crud/src/components/pagination/index.tsx index 9aca89f40..db51407ef 100644 --- a/packages/crud/src/components/pagination/index.tsx +++ b/packages/crud/src/components/pagination/index.tsx @@ -68,7 +68,7 @@ export default defineComponent({ return () => { return h( { + console.error(err); + }) loading.value = false; } @@ -140,7 +143,6 @@ export default defineComponent({
0} diff --git a/packages/crud/src/components/search/index.tsx b/packages/crud/src/components/search/index.tsx index 360814400..8db3d958c 100644 --- a/packages/crud/src/components/search/index.tsx +++ b/packages/crud/src/components/search/index.tsx @@ -1,7 +1,20 @@ -import { useConfig, useCore, useForm } from "../../hooks"; -import { isEmpty } from "lodash-es"; -import { onMounted, PropType, defineComponent, ref, h, reactive, inject, mergeProps } from "vue"; +import { useConfig, useCore, useForm, useProxy, useRefs } from "../../hooks"; +import { + onMounted, + PropType, + defineComponent, + ref, + h, + reactive, + inject, + mergeProps, + nextTick, + onUnmounted +} from "vue"; import { useApi } from "../form/helper"; +import { Search, Refresh, Bottom, Top } from "@element-plus/icons-vue"; +import { mitt } from "../../utils/mitt"; +import { isArray, isObject, isString } from "lodash-es"; export default defineComponent({ name: "cl-search", @@ -13,7 +26,7 @@ export default defineComponent({ }, props: { type: Object, - default: () => {} + default: () => { } }, // 表单值 @@ -36,6 +49,9 @@ export default defineComponent({ default: false }, + // 是否需要折叠 + collapse: Boolean, + // 初始化 onLoad: Function, @@ -47,6 +63,7 @@ export default defineComponent({ setup(props, { slots, expose, emit }) { const { crud } = useCore(); + const { refs, setRefs } = useRefs() const { style } = useConfig(); // 配置 @@ -60,6 +77,12 @@ export default defineComponent({ // 加载中 const loading = ref(false); + // 展开 + const isExpand = ref(!config.collapse); + + // 显示展开、收起按钮 + const showExpandBtn = ref(false); + // 搜索 function search(params?: any) { const form = Form.value?.getForm(); @@ -99,6 +122,16 @@ export default defineComponent({ const d: any = {}; config.items?.map((e) => { + if (typeof e.hook != 'string' && e.hook?.reset) { + const props = e.hook.reset(e.prop!) + + if (isArray(props)) { + props.forEach((prop) => { + d[prop] = undefined; + }) + } + } + d[e.prop!] = undefined; }); @@ -106,19 +139,49 @@ export default defineComponent({ Form.value?.reset(); // 列表刷新 - crud.refresh(d); + search(d); // 重置事件 emit("reset", d); } - expose({ + // 收起、展开 + function expand() { + isExpand.value = !isExpand.value; + + nextTick(() => { + crud?.["cl-table"].calcMaxHeight() + }) + } + + // 判断展开状态 + function onExpand() { + if (config.collapse) { + const el = refs.form?.querySelector(".cl-form__items"); + + if (el) { + showExpandBtn.value = el.clientHeight > 84; + } + } + } + + function onResize() { + onExpand(); + } + + const ctx = { search, reset, + Form, + config, ...useApi({ Form }) - }); + }; + + useProxy(ctx); + expose(ctx); onMounted(() => { + // 打开表单 Form.value?.open({ op: { hidden: true @@ -129,15 +192,51 @@ export default defineComponent({ on: { open(data) { config.onLoad?.(data); + onExpand(); + }, + change(data, prop) { + config.onChange?.(data, prop); } } }); + + mitt.on("resize", onResize); }); + onUnmounted(() => { + mitt.off("resize", onResize); + }) + return () => { + const btnEl = ( + + {/* 重置按钮 */} + {config.resetBtn && ( + + {crud.dict.label.reset} + + )} + + {/* 搜索按钮 */} + { + search(); + }}> + {crud.dict.label.search} + + + {/* 自定义按钮 */} + {slots?.buttons?.(Form.value?.form)} + + ); + return ( - isEmpty(config.items) || ( -