diff --git a/packages/base/data/listTableDialog.vue b/packages/base/data/listTableDialog.vue new file mode 100644 index 0000000..4ec89a3 --- /dev/null +++ b/packages/base/data/listTableDialog.vue @@ -0,0 +1,102 @@ + + + diff --git a/packages/base/index.ts b/packages/base/index.ts index b3aee61..a9a9d75 100644 --- a/packages/base/index.ts +++ b/packages/base/index.ts @@ -1,16 +1,32 @@ -import NoobTag from './item/tag.vue'; -import NoobButton from './item/button.vue'; -import NoobSelect from './item/select.vue'; -import NoobInput from './item/input.vue'; -import NoobDate from './item/datetime.vue'; -import LightBox from './item/light-box.vue'; +import NoobTag from "./item/tag.vue"; +import NoobButton from "./item/button.vue"; +import NoobSelect from "./item/select.vue"; +import NoobInput from "./item/input.vue"; +import NoobDate from "./item/datetime.vue"; +import LightBox from "./item/light-box.vue"; +import ButtonWithTooltip from "./item/buttonWithTooltip.vue"; +import ConfirmCancel from "./item/confirmCancel.vue"; +import TzDatePicker from "./item/tzDatePicker.vue"; +import TzDateTime from "./item/tzDateTime.vue"; +import SearchRow from "./data/search-row.vue"; +import ListTable from "./data/list-table.vue"; +import Infomation from "./data/infomation.vue"; +import ModifyForm from "./data/modify-form.vue"; +import Descriptions from "./data/descriptions.vue"; +import TableAction from "./data/table-action.vue"; +import ListTableDialog from "./data/listTableDialog.vue"; -export { NoobTag, NoobButton, NoobSelect, NoobInput, NoobDate, LightBox }; +export { + NoobTag, + NoobButton, + NoobSelect, + NoobInput, + NoobDate, + LightBox, + ButtonWithTooltip, + TzDateTime, + TzDatePicker, + ConfirmCancel, +}; -import SearchRow from './data/search-row.vue'; -import ListTable from './data/list-table.vue'; -import Infomation from './data/infomation.vue'; -import ModifyForm from './data/modify-form.vue'; -import Descriptions from './data/descriptions.vue'; -import TableAction from './data/table-action.vue'; -export { SearchRow, ListTable, Infomation, ModifyForm, Descriptions, TableAction }; +export { SearchRow, ListTable, ListTableDialog, Infomation, ModifyForm, Descriptions, TableAction }; diff --git a/packages/base/item/buttonWithTooltip.vue b/packages/base/item/buttonWithTooltip.vue new file mode 100644 index 0000000..12c073f --- /dev/null +++ b/packages/base/item/buttonWithTooltip.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/packages/base/item/confirmCancel.vue b/packages/base/item/confirmCancel.vue new file mode 100644 index 0000000..b4dbd53 --- /dev/null +++ b/packages/base/item/confirmCancel.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/base/item/datetime.vue b/packages/base/item/datetime.vue index f9adc0b..f897665 100644 --- a/packages/base/item/datetime.vue +++ b/packages/base/item/datetime.vue @@ -1,73 +1,87 @@ \ No newline at end of file + diff --git a/packages/base/item/tzDatePicker.vue b/packages/base/item/tzDatePicker.vue new file mode 100644 index 0000000..3677920 --- /dev/null +++ b/packages/base/item/tzDatePicker.vue @@ -0,0 +1,54 @@ + + + diff --git a/packages/base/item/tzDateTime.vue b/packages/base/item/tzDateTime.vue new file mode 100644 index 0000000..ff9c0db --- /dev/null +++ b/packages/base/item/tzDateTime.vue @@ -0,0 +1,55 @@ + + diff --git a/packages/echarts/line/smoothed.vue b/packages/echarts/line/smoothed.vue index 93aaf22..7271782 100644 --- a/packages/echarts/line/smoothed.vue +++ b/packages/echarts/line/smoothed.vue @@ -2,78 +2,84 @@ \ No newline at end of file + diff --git a/packages/manage/router/index.vue b/packages/manage/router/index.vue index 73c8198..80cb250 100644 --- a/packages/manage/router/index.vue +++ b/packages/manage/router/index.vue @@ -1,247 +1,241 @@ - \ No newline at end of file + diff --git a/packages/manage/router/zhuBeiDong.vue b/packages/manage/router/zhuBeiDong.vue index 4037626..7af4004 100644 --- a/packages/manage/router/zhuBeiDong.vue +++ b/packages/manage/router/zhuBeiDong.vue @@ -1,239 +1,234 @@ - \ No newline at end of file + diff --git a/packages/manage/views/scope.vue b/packages/manage/views/scope.vue new file mode 100644 index 0000000..63cf316 --- /dev/null +++ b/packages/manage/views/scope.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/plugs/composables/index.ts b/plugs/composables/index.ts new file mode 100644 index 0000000..8e28f4b --- /dev/null +++ b/plugs/composables/index.ts @@ -0,0 +1,6 @@ +export * from "./useActionPers"; +export * from "./useListTable"; +export * from "./useLoading"; +export * from "./useModifyForm"; +export * from "./useSysDict"; +export * from "./useWatchOnce"; diff --git a/plugs/composables/useActionPers.ts b/plugs/composables/useActionPers.ts new file mode 100644 index 0000000..d880cae --- /dev/null +++ b/plugs/composables/useActionPers.ts @@ -0,0 +1,32 @@ +import { onMounted, toRef } from "vue"; +import { useRoute } from "vue-router"; +import { useStore } from "vuex"; + +export function useActionPers(parent?: string) { + const store = useStore(); + + if (!parent) { + const route = useRoute(); + parent = route.path; + } + + const update = async () => { + await store.dispatch("updateActionPers", parent); + }; + + const actionPers = toRef(() => { + // @ts-ignore + const pers = store.state.actionPers[parent] ?? []; + return Object.fromEntries(pers.map((code) => [code, true])); + }); + + onMounted(async () => { + await update(); + }); + + return { + get: (per) => !!actionPers.value[per], + update, + actionPers, + }; +} diff --git a/plugs/composables/useListTable.ts b/plugs/composables/useListTable.ts new file mode 100644 index 0000000..3ba81f0 --- /dev/null +++ b/plugs/composables/useListTable.ts @@ -0,0 +1,86 @@ +import { toReactive } from "@vueuse/core"; +import { reactive, ref, shallowRef, watchEffect } from "vue"; +import * as Element from "../element"; +import { useI18n } from "vue3-i18n"; +import { PageResponse } from "../http"; + +const { showMessage } = Element; + +export interface TableColumn { + code: string; + name?: string; + i18n?: string; + width: number; + dict?: string; + slot?: boolean; + + [others: string]: any; +} + +export interface TableData { + rows: PageResponse; + example: Record; +} + +interface Options { + query: (example: any) => Promise | Record[] | undefined>; + initialPage?: number; + initialPageSize?: number; + initExample?: Record; + disableAutoQuery?: boolean; + deep?: boolean; +} + +export function useListTable(options: Options) { + const { t } = useI18n(); + const { initialPage, initialPageSize, initExample, deep } = options; + + type Row = Record; + type Rows = Row[] | PageResponse; + + const empty = () => [] as T[]; + const rows = deep ? ref(empty()) : shallowRef(empty()); + + const setRows = (resp?: Rows) => { + if (resp == null) { + rows.value = empty(); + } else { + rows.value = resp; + if (Array.isArray(resp) || resp.total == null) { + for (const key in pageParams) { + delete example[key]; + } + } + } + }; + + const pageParams = { + page: initialPage || 1, + size: initialPageSize || 10, + }; + + const example = reactive({ + ...pageParams, + ...initExample, + }); + + const query = async () => { + try { + const resp = await options.query(example); + setRows(resp); + } catch (error) { + showMessage("error", t("common.errors.listTableQueryError")); + } + }; + + if (!options.disableAutoQuery) { + watchEffect(query); + } + + return { + rows: toReactive(rows), + example, + setRows, + query, + }; +} diff --git a/plugs/composables/useLoading.ts b/plugs/composables/useLoading.ts new file mode 100644 index 0000000..3121ba4 --- /dev/null +++ b/plugs/composables/useLoading.ts @@ -0,0 +1,18 @@ +// composables/useLoading.js +import { ElLoading } from 'element-plus' + +export function useLoading() { + const showLoading = (options = {}) => { + return ElLoading.service({ + lock: true, + text: 'Loading', + spinner: 'el-icon-loading', + background: 'rgba(0, 0, 0, 0.3)', + ...options + }) + } + + return { + showLoading + } +} diff --git a/plugs/composables/useModifyForm.ts b/plugs/composables/useModifyForm.ts new file mode 100644 index 0000000..883dd64 --- /dev/null +++ b/plugs/composables/useModifyForm.ts @@ -0,0 +1,132 @@ +import { reactive, Ref } from "vue"; +import {} from "noob-mengyxu"; +import * as Element from "../element"; +import { clearAndAssign, deepCopy } from "../util/objectUtil"; + +const { showMessage } = Element; + +export interface FormProp { + code: string; + name?: string; + i18n?: string; + dict?: string; + slot?: boolean; + + [others: string]: any; +} + +interface Options { + props?: FormProp[]; + formRef?: Ref; + handleAdd?: (value) => Promise; + handleEdit?: (value) => Promise; + handleCancel?: () => Promise; + handleError?: (kind: "validation" | "internal", err?) => void; + init?: any; + extraProps?: string[] | "any" | ((code: string) => boolean); +} + +export function useModifyForm(options: Options) { + const { formRef, handleAdd, handleEdit, handleCancel, init, extraProps } = options; + const initValue = init ?? {}; + const props = options.props ?? []; + const propMap = Object.fromEntries(props.map((prop) => [prop.code, prop])); + + let isValidProp: (code: string) => boolean; + + if (typeof extraProps === "function") { + isValidProp = (code: string) => extraProps(code) as boolean; + } else if (Array.isArray(extraProps)) { + isValidProp = (code: string) => extraProps.includes(code); + } else { + isValidProp = (_code: string) => extraProps === "any"; + } + + const normalizeValue = (code: string, value: any) => { + const prop = propMap[code]; + if (!prop && !isValidProp(code)) return null; + if (prop?.dict) { + const parsed = parseInt(value); + return isNaN(parsed) ? value : parsed; + } + + return value; + }; + + const normalizeModel = (newModel: Record) => + Object.fromEntries( + Object.entries(newModel).flatMap(([code, value]) => { + const normalized = normalizeValue(code, value); + return normalized != null ? [[code, normalized]] : []; + }) + ); + + const handleError = + options.handleError ?? + ((kind, err) => { + if (kind === "validation") { + showMessage("error", "Validation Error"); + } else if (kind === "internal") { + const msg = `Internal Error: ${err}`; + showMessage("error", msg); + console.error(msg); + } + }); + + return { + flagOnClose: null, + flagOnAdd: "add", + flagOnEdit: "edit", + + data: reactive<{ dialog: any; model: Record }>({ + dialog: null, + model: { ...initValue }, + }), + + clearModel() { + clearAndAssign(this.data.model, initValue); + }, + + assignModel(newModel: Record) { + const normalized = normalizeModel(newModel); + this.clearModel(); + Object.assign(this.data.model, normalized); + }, + + openAdd() { + this.clearModel(); + formRef?.value?.clearValidate(); + this.data.dialog = this.flagOnAdd; + }, + + openEdit(toEdit: Record) { + this.assignModel(toEdit); + formRef?.value?.clearValidate(); + this.data.dialog = this.flagOnEdit; + }, + + close() { + this.data.dialog = this.flagOnClose; + }, + + async onConfirm() { + const value = deepCopy(this.data.model); + try { + if (this.data.dialog === this.flagOnAdd) { + await handleAdd?.(value); + } else if (this.data.dialog === this.flagOnEdit) { + await handleEdit?.(value); + } + } catch (err) { + handleError?.("internal", err); + } + + this.close(); + }, + + async onCancel() { + await handleCancel?.(); + this.close(); + }, + }; +} diff --git a/plugs/composables/useSysDict.ts b/plugs/composables/useSysDict.ts new file mode 100644 index 0000000..7bc33b7 --- /dev/null +++ b/plugs/composables/useSysDict.ts @@ -0,0 +1,43 @@ +import { computed } from "vue"; +import { useStore } from "vuex"; + +export function useSysDict() { + type Callback = () => Record | Promise>; + const store = useStore(); + const callbacks: Record = {}; + + const normalize = (content: Record) => { + if (content == null) return {}; + return Object.fromEntries( + Object.entries(content).map(([k, v]) => { + const parsed = parseInt(v); + return [k, isNaN(parsed) ? v : parsed]; + }) + ); + }; + + const updateDict = async (dictIds: string[], registerCallbacks: Record = {}) => { + await store.dispatch("getDictMap", dictIds); + Object.assign(callbacks, registerCallbacks); + + for (const id of dictIds) { + const cb = callbacks[id]; + const resp = await cb?.(); + if (resp) { + store.commit("updateDict", [id, normalize(resp)]); + } + } + }; + + const unregister = (ids: string[]) => { + for (const id of ids) { + delete callbacks[id]; + } + }; + + return { + sysDict: computed>>(() => store.state.dict), + updateDict, + unregister, + }; +} diff --git a/plugs/composables/useWatchOnce.ts b/plugs/composables/useWatchOnce.ts new file mode 100644 index 0000000..cb05695 --- /dev/null +++ b/plugs/composables/useWatchOnce.ts @@ -0,0 +1,12 @@ +import { nextTick, Ref, watchEffect, WatchEffect } from "vue"; + +export function watchEffectOnce(cond: Ref, cb: WatchEffect) { + const stop = watchEffect((onCleanup) => { + if (cond.value) { + nextTick(() => stop()); + cb(onCleanup); + } + }); + + return stop; +} diff --git a/plugs/element/listTableDialog.ts b/plugs/element/listTableDialog.ts new file mode 100644 index 0000000..14e5192 --- /dev/null +++ b/plugs/element/listTableDialog.ts @@ -0,0 +1,68 @@ +import { type TableColumn, ListTableDialog, handleAsync } from "noob-mengyxu"; +import { ElMessageBox } from "element-plus"; +import { h } from "vue"; +import { PageResponse } from "../http"; +type Row = Record; + +export interface ListTableProps { + props: TableColumn[]; + rowKey: string; + initialPage?: number; + initialPageSize?: number; + initExample?: Record; + useDicts?: string[]; +} + +interface Options { + title: string; + props: ListTableProps; + query: (example: Record) => Promise>; + confirm: (rows: Row[]) => Promise; + children?: any; +} + +export function showListTableDialog({ title, props, query, confirm, children }: Options) { + const open = async () => { + try { + await ElMessageBox({ + title, + showConfirmButton: false, + showCancelButton: false, + message: h( + ListTableDialog, + { + ...props, + onQuery: handleAsync(query), + onConfirm: handleAsync(async (rows) => { + console.log("onConfirm"); + await confirm(rows); + }), + onClose: handleAsync(async () => { + console.log("onClose"); + ElMessageBox.close(); + }), + }, + children + ), + customStyle: { + maxWidth: "80%", + }, + }); + } catch (err) { + if (err === "cancel" || err === "close") { + return; + } + + throw err; + } + }; + + const close = () => { + ElMessageBox.close(); + }; + + return { + open, + close, + }; +} diff --git a/plugs/http/axios3.ts b/plugs/http/axios3.ts new file mode 100644 index 0000000..fbb8170 --- /dev/null +++ b/plugs/http/axios3.ts @@ -0,0 +1,318 @@ +/* eslint-disable */ +import axios from "axios"; +import * as Lang from "../i18n"; +import { loading, close, showMessage } from "../element"; +const { t } = Lang.i18n; + +let router; + +const config = { + baseURL: process.env.VUE_APP_BASE_URL ? "/api" : "", + timeout: 60 * 1000, + withCredentials: true, // Check cross-site Access-Control +}; + +const _axios = axios.create(config); +_axios.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8"; +_axios.defaults.headers.put["Content-Type"] = "application/json;charset=UTF-8"; +_axios.defaults.headers.delete["Content-Type"] = "application/json;charset=UTF-8"; + +export const registerBaseUrl = (url) => { + _axios.defaults.baseURL = url; +}; + +export const registerRouter = (routerP) => { + router = routerP; +}; + +// Add a request interceptor +_axios.interceptors.request.use( + function (config) { + const time = new Date().getTime().toString(); + const params = config.params; + if (params) { + delEmpty(params); + params.t = time; + } + const data = config.data; + if (data != null && typeof data === "object") { + delEmpty(data); + data.t = time; + } + return config; + }, + function (error) { + return Promise.reject(error); + } +); + +function delEmpty(data) { + if (data) { + for (const item in data) { + if (data.hasOwnProperty(item)) { + const val = data[item]; + if ((val == null || val == "null" || val == "") && val !== 0 && val !== false) { + delete data[item]; + } + } + } + } +} + +// Add a response interceptor +_axios.interceptors.response.use( + function (response) { + return response.data; + }, + function (error) { + return Promise.reject(error); + } +); + +type QueryParams = URLSearchParams | Record | ConstructorParameters[0]; + +export interface AxiosOptions { + noMsg?: boolean | null; + noLoading?: boolean | null; + filter?: (response) => any; + query?: QueryParams; +} + +const defaultFilterResp = (resp) => { + const isSuccess = resp.success || resp.status === "ok" || resp.status === "success" || resp.data != null; + const isError = resp.success === false || resp.status === "error"; + if (isSuccess) { + return resp.data ? resp.data : resp; + } else if (isError) { + return false; + } else return resp; +}; + +export function post(url, data?, options: AxiosOptions = {}) { + return new Promise((resolve, reject) => { + if (!options.noLoading) { + loading(); + } + _axios.post(url, data).then( + (response: any) => { + handResponse(response, resolve, options); + }, + (err) => { + handError(err, reject, options); + } + ); + }); +} + +function mergeQueryParams(...params: Array) { + const res: URLSearchParams = new URLSearchParams(); + for (const param of params) { + if (param == null) continue; + let parsed; + if (param instanceof URLSearchParams) { + parsed = param; + } else { + parsed = new URLSearchParams(param); + } + + parsed + .entries() + .filter(([k, v]) => v != null && v != "null" && v != "undefined" && v != "") + .forEach(([k, v]) => res?.append(k, v)); + } + + return res ?? new URLSearchParams(); +} + +export function get(url, data?: QueryParams, options: AxiosOptions = {}) { + return new Promise((resolve, reject) => { + if (!options.noLoading) { + loading(); + } + _axios.get(url, { params: mergeQueryParams(data, options.query) }).then( + (response: any) => { + handResponse(response, resolve, options); + }, + (err) => { + handError(err, reject, options); + } + ); + }); +} + +export function put(url, data?, options: AxiosOptions = {}) { + return new Promise((resolve, reject) => { + if (!options.noLoading) { + loading(); + } + _axios.put(url, data, { params: mergeQueryParams(options.query) }).then( + (response: any) => { + handResponse(response, resolve, options); + }, + (err) => { + handError(err, reject, options); + } + ); + }); +} + +export function delate(url, data?, options: AxiosOptions = {}) { + return new Promise((resolve, reject) => { + if (!options.noLoading) { + loading(); + } + _axios({ + method: "delete", + url: url, + data: data, + params: mergeQueryParams(options.query), + }).then( + (response: any) => { + handResponse(response, resolve, options); + }, + (err) => { + handError(err, reject, options); + } + ); + }); +} + +function handResponse(response, resolve, options: AxiosOptions = {}) { + if (!options.noLoading) { + close(); + } + + const filterResponse = options.filter ?? defaultFilterResp; + const result = filterResponse(response); + + if (result) { + if (!options.noMsg && response.message) { + showMessage("success", response.message); + } + + resolve(result); + } else { + if (response.message == "session timeout") { + router?.push("/login"); + response.message = t("http.unLogin"); + } + if (response.message == "no permission") { + response.message = t("http.unPermission"); + } + if (response.message?.indexOf("no permission for ") == 0) { + response.message = t("http.noPermission"); + } + if (!options.noMsg && response.message) { + showMessage("error", response.message); + } + response.error && console.log(response.error); + resolve(false); + } +} + +function handError(err, reject, options: AxiosOptions = {}) { + if (!options.noLoading) { + close(); + } + if (!options.noMsg) { + showMessage("error", t("http.error")); + } + reject(err); +} + +export function upload(file, url, data) { + return new Promise((resolve, reject) => { + let param = new FormData(); // 创建form对象 + param.append("file", file); // 通过append向form对象添加数据 + if (data) { + for (const item in data) { + if (data.hasOwnProperty(item)) { + param.append(item, data[item]); // 添加form表单中其他数据 + } + } + } + let config = { + headers: { "Content-Type": "multipart/form-data" }, + }; + _axios.post(url, param, config).then( + (response) => { + handResponse(response, resolve); + }, + (err) => { + handError(err, reject); + } + ); + }); +} + +export function getFile(url, data?, options: AxiosOptions = {}) { + return new Promise((resolve, reject) => { + if (!options.noLoading) { + loading(); + } + _axios({ + method: "get", + url: url, // 请求地址 + params: data, // 参数 + responseType: "blob", // 表明返回服务器返回的数据类型 + }).then( + (response: any) => { + handResponse(response, resolve, options); + }, + (err) => { + handError(err, reject, options); + } + ); + }); +} + +export function download(fileName, url, data = {}, callBack?) { + loading(); + _axios({ + method: "get", + url: url, // 请求地址 + params: data, // 参数 + responseType: "blob", // 表明返回服务器返回的数据类型 + }).then( + (response: any) => { + close(); + const reader = new FileReader() as any; + reader.readAsText(response); + reader.onload = function () { + try { + const result = JSON.parse(reader.result); + if (typeof result === "object") { + showMessage("error", result.message, false); + if (callBack != null) { + callBack(false); + } + return; + } + } catch (err) {} + const blob = new Blob([response], { + type: "application/octet-stream", + }); + const navigator = window.navigator as any; + if (navigator.msSaveOrOpenBlob) { + navigator.msSaveBlob(blob, fileName); + } else { + const link = document.createElement("a"); + link.href = window.URL.createObjectURL(blob); + link.download = fileName; + link.click(); + window.URL.revokeObjectURL(link.href); + } + if (callBack != null) { + callBack(true); + } + }; + }, + (err) => { + close(); + showMessage("error", t("http.downFail")); + if (callBack != null) { + callBack(false); + } + } + ); +} diff --git a/plugs/http/index.ts b/plugs/http/index.ts index 57c1c24..74a6fc2 100644 --- a/plugs/http/index.ts +++ b/plugs/http/index.ts @@ -1,2 +1,4 @@ -export * as Axios from './axios'; -export * as Axios2 from './axios2'; +export * as Axios from "./axios"; +export * as Axios2 from "./axios2"; +export * as Axios3 from "./axios3"; +export * from "./misc"; diff --git a/plugs/http/misc.ts b/plugs/http/misc.ts new file mode 100644 index 0000000..9ea829d --- /dev/null +++ b/plugs/http/misc.ts @@ -0,0 +1,10 @@ +export interface PageResponse { + data: T[]; + page?: number; + total: number; +} + +export const pageEmpty = (): PageResponse => ({ + data: [], + total: 0, +}); diff --git a/plugs/index.ts b/plugs/index.ts index c5fcb54..257495e 100644 --- a/plugs/index.ts +++ b/plugs/index.ts @@ -1,7 +1,10 @@ -export * from './config'; -export * as Element from './element'; -export * as Store from './store'; -export * as Http from './http'; -export * as Lang from './i18n'; -export * as Api from './api'; -export * from './constant'; +export * from "./config"; +export * as Element from "./element"; +export * as Store from "./store"; +export * as Http from "./http"; +export * as Lang from "./i18n"; +export * as Api from "./api"; +export * from "./constant"; +export * from "./util/asyncUtil"; +export * from "./util/objectUtil"; +export * from "./composables"; diff --git a/plugs/store/index.ts b/plugs/store/index.ts index af19b85..66eaf5b 100644 --- a/plugs/store/index.ts +++ b/plugs/store/index.ts @@ -16,6 +16,8 @@ export class State { style = Styles.plain; size = Size.normal; actions = []; + user = {}; + actionPers = {}; } export class Actions { diff --git a/plugs/util/asyncUtil.ts b/plugs/util/asyncUtil.ts new file mode 100644 index 0000000..5e32b7b --- /dev/null +++ b/plugs/util/asyncUtil.ts @@ -0,0 +1,21 @@ +export type AsyncHandler = { + resolve: (t: T | PromiseLike) => void; + reject: (err: any) => void; +}; + +type ExtractResult = Args extends [AsyncHandler, ...any[]] ? T : never; + +type ExtractArgs = Args extends [AsyncHandler, ...infer Rest] ? Rest : never; + +export function useAsyncEmits>(emits: (evt: any, ...args: any[]) => void) { + const emitsAsync = (evt: Evt, ...args: ExtractArgs) => + new Promise>((resolve, reject) => { + emits(evt, { resolve, reject }, ...args); + }); + + return emitsAsync; +} + +export function handleAsync(handler: (...args: Args) => Promise) { + return ({ resolve, reject }: AsyncHandler, ...args: Args) => handler(...args).then(resolve, reject); +} diff --git a/plugs/util/objectUtil.ts b/plugs/util/objectUtil.ts new file mode 100644 index 0000000..926de7a --- /dev/null +++ b/plugs/util/objectUtil.ts @@ -0,0 +1,22 @@ +import { cloneDeep } from "lodash-es"; + +export function deepCopy(obj: T): T { + return cloneDeep(obj); +} + +export function clearObject(obj: Record) { + for (const key in obj) { + delete obj[key]; + } +} + +export function clearAndAssign(target: Record, source: Record) { + clearObject(target); + Object.assign(target, source); +} + +export function unnest(obj: Record, key: string, prefix: string) { + const { [key]: toFlatten, ...rest } = obj; + const prefixed = Object.fromEntries(Object.entries(toFlatten).map(([k, v]) => [prefix.concat(k), v])); + return { ...rest, ...prefixed }; +}