forked from mengyxu/noob-components
26 changed files with 1581 additions and 491 deletions
@ -0,0 +1,102 @@ |
|||||||
|
<template> |
||||||
|
<el-container> |
||||||
|
<el-main> |
||||||
|
<ListTable |
||||||
|
page |
||||||
|
border |
||||||
|
:data="tableData.rows" |
||||||
|
:props="propsWithSelection" |
||||||
|
:row-key="rowKey" |
||||||
|
:example="tableData.example" |
||||||
|
@query="tableData.query()" |
||||||
|
> |
||||||
|
<template v-for="{ code } in propsWithSlot" #[code]="{ row }"> |
||||||
|
<slot :name="code" :row /> |
||||||
|
</template> |
||||||
|
<template #ui_selection="{ row }"> |
||||||
|
<el-checkbox |
||||||
|
:value="selection.get(row[rowKey])" |
||||||
|
@update:model-value=" |
||||||
|
(val) => { |
||||||
|
if (val) { |
||||||
|
selection.set(row[rowKey], row); |
||||||
|
} else { |
||||||
|
selection.delete(row[rowKey]); |
||||||
|
} |
||||||
|
} |
||||||
|
" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
</ListTable> |
||||||
|
</el-main> |
||||||
|
<el-footer> |
||||||
|
<ConfirmCancel @confirm="handleConfirm" @cancel="handleCancel" /> |
||||||
|
</el-footer> |
||||||
|
</el-container> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { ListTable, Element, AsyncHandler, useAsyncEmits, useListTable, useSysDict, ConfirmCancel } from "noob-mengyxu"; |
||||||
|
import { computed, onMounted, reactive, ref } from "vue"; |
||||||
|
import { useStore } from "vuex"; |
||||||
|
import { useI18n } from "vue3-i18n"; |
||||||
|
import { ListTableProps } from "../../../plugs/element/listTableDialog"; |
||||||
|
import { PageResponse } from "../../../plugs/http"; |
||||||
|
const { showMessage } = Element; |
||||||
|
const { t } = useI18n(); |
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false }); |
||||||
|
|
||||||
|
type Row = Record<string, any>; |
||||||
|
|
||||||
|
const p = defineProps<ListTableProps>(); |
||||||
|
|
||||||
|
type Emits = { |
||||||
|
query: [handler: AsyncHandler<PageResponse<Row>>, example: Record<string, any>]; |
||||||
|
confirm: [handler: AsyncHandler<void>, rows: Row[]]; |
||||||
|
close: [handler: AsyncHandler<void>]; |
||||||
|
}; |
||||||
|
const emits = defineEmits<Emits>(); |
||||||
|
const emitsAsync = useAsyncEmits<Emits>(emits); |
||||||
|
|
||||||
|
const propsWithSelection = computed(() => [ |
||||||
|
{ code: "ui_selection", slot: true, i18n: "common.select", width: 80 }, |
||||||
|
...p.props, |
||||||
|
]); |
||||||
|
const propsWithSlot = computed(() => p.props.filter((col) => col.slot)); |
||||||
|
|
||||||
|
const store = useStore(); |
||||||
|
|
||||||
|
const { sysDict, updateDict } = useSysDict(); |
||||||
|
|
||||||
|
const tableData = useListTable({ |
||||||
|
query: (example) => emitsAsync("query", example), |
||||||
|
initialPage: p.initialPage, |
||||||
|
initialPageSize: p.initialPageSize, |
||||||
|
initExample: p.initExample, |
||||||
|
disableAutoQuery: true, |
||||||
|
}); |
||||||
|
|
||||||
|
const selection = reactive(new Map()); |
||||||
|
|
||||||
|
const handleConfirm = async () => { |
||||||
|
try { |
||||||
|
await emitsAsync("confirm", Array.from(selection.values())); |
||||||
|
await emitsAsync("close"); |
||||||
|
} catch { |
||||||
|
showMessage("error", t("common.errors.submitError")); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handleCancel = async () => { |
||||||
|
await emitsAsync("close"); |
||||||
|
}; |
||||||
|
|
||||||
|
onMounted(async () => { |
||||||
|
if (p.useDicts) { |
||||||
|
await updateDict(p.useDicts); |
||||||
|
} |
||||||
|
|
||||||
|
await tableData.query(); |
||||||
|
}); |
||||||
|
</script> |
||||||
@ -1,16 +1,32 @@ |
|||||||
import NoobTag from './item/tag.vue'; |
import NoobTag from "./item/tag.vue"; |
||||||
import NoobButton from './item/button.vue'; |
import NoobButton from "./item/button.vue"; |
||||||
import NoobSelect from './item/select.vue'; |
import NoobSelect from "./item/select.vue"; |
||||||
import NoobInput from './item/input.vue'; |
import NoobInput from "./item/input.vue"; |
||||||
import NoobDate from './item/datetime.vue'; |
import NoobDate from "./item/datetime.vue"; |
||||||
import LightBox from './item/light-box.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'; |
export { SearchRow, ListTable, ListTableDialog, Infomation, ModifyForm, Descriptions, TableAction }; |
||||||
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 }; |
|
||||||
|
|||||||
@ -0,0 +1,46 @@ |
|||||||
|
<template> |
||||||
|
<el-tooltip |
||||||
|
v-if="props.tooltip" |
||||||
|
:content="props.tooltip" |
||||||
|
:disabled="props.disabled" |
||||||
|
:show-after="0" |
||||||
|
:hide-after="0" |
||||||
|
:popper-options="{ modifiers: [{ name: 'eventListeners', enabled: false }] }" |
||||||
|
popper-class="non-interactive-tooltip" |
||||||
|
> |
||||||
|
<NoobButton :disabled="props.disabled" v-bind="buttonProps"> |
||||||
|
<slot /> |
||||||
|
</NoobButton> |
||||||
|
</el-tooltip> |
||||||
|
<NoobButton v-else :disabled="props.disabled" v-bind="buttonProps"> |
||||||
|
<slot /> |
||||||
|
</NoobButton> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { useAttrs } from "vue"; |
||||||
|
import { ElTooltip } from "element-plus"; |
||||||
|
import { computed } from "vue"; |
||||||
|
import { NoobButton } from "noob-mengyxu"; |
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false }); |
||||||
|
|
||||||
|
interface Props { |
||||||
|
tooltip?: string; |
||||||
|
disabled: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
const props = defineProps<Props>(); |
||||||
|
const attrs = useAttrs(); |
||||||
|
|
||||||
|
const buttonProps = computed(() => { |
||||||
|
const { class: _, ...rest } = attrs; |
||||||
|
return rest; |
||||||
|
}); |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.non-interactive-tooltip { |
||||||
|
pointer-events: none !important; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
<template> |
||||||
|
<el-row> |
||||||
|
<NoobButton type="primary" @click="emit('confirm')">{{ |
||||||
|
t("base.confirm") |
||||||
|
}}</NoobButton> |
||||||
|
<el-space fill /> |
||||||
|
<NoobButton type="info" @click="emit('cancel')">{{ |
||||||
|
t("base.cancel") |
||||||
|
}}</NoobButton> |
||||||
|
</el-row> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { NoobButton } from "noob-mengyxu"; |
||||||
|
import { useI18n } from "vue3-i18n"; |
||||||
|
const { t } = useI18n(); |
||||||
|
|
||||||
|
const emit = defineEmits<{ |
||||||
|
(e: "confirm"): void | Promise<void>; |
||||||
|
(e: "cancel"): void | Promise<void>; |
||||||
|
}>(); |
||||||
|
</script> |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
<template> |
||||||
|
<el-date-picker v-model="model"> </el-date-picker> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { ElDatePicker } from "element-plus"; |
||||||
|
import dayjs, { type Dayjs } from "dayjs"; |
||||||
|
import { nextTick, toRef, watch, watchEffect } from "vue"; |
||||||
|
import { watchEffectOnce } from "../../../plugs/composables"; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
valueType: "iso8601" | "unix" | "unixMillis"; |
||||||
|
tz?: string; |
||||||
|
keepMillis?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
const props = defineProps<Props>(); |
||||||
|
|
||||||
|
watchEffectOnce(toRef(props, "tz"), async () => { |
||||||
|
const timeZone = await import("dayjs/plugin/timezone"); |
||||||
|
dayjs.extend(timeZone.default); |
||||||
|
}); |
||||||
|
|
||||||
|
const model = defineModel<string | number, string, Date, Date>({ |
||||||
|
get: (value) => { |
||||||
|
let dt: Dayjs; |
||||||
|
if (props.valueType === "unix" || props.valueType === "unixMillis") { |
||||||
|
if (typeof value === "string") { |
||||||
|
value = parseFloat(value); |
||||||
|
} |
||||||
|
dt = props.valueType === "unix" ? dayjs.unix(value) : dayjs(value); |
||||||
|
} else { |
||||||
|
dt = dayjs(value); |
||||||
|
} |
||||||
|
|
||||||
|
return dt.toDate(); |
||||||
|
}, |
||||||
|
set: (value) => { |
||||||
|
const dt = dayjs(value); |
||||||
|
if (props.valueType === "unix") { |
||||||
|
return dt.unix(); |
||||||
|
} else if (props.valueType === "unixMillis") { |
||||||
|
return dt.valueOf(); |
||||||
|
} else if (props.valueType === "iso8601" && props.tz) { |
||||||
|
const fmt = props.keepMillis ? "YYYY-MM-DDTHH:mm:ss.SSSZ" : "YYYY-MM-DDTHH:mm:ssZ"; |
||||||
|
return dt.tz(props.tz).format(fmt); |
||||||
|
} else { |
||||||
|
const isoWithMillis = dt.toISOString(); |
||||||
|
if (props.keepMillis) return isoWithMillis; |
||||||
|
else return isoWithMillis.slice(0, 19).concat("Z"); |
||||||
|
} |
||||||
|
}, |
||||||
|
}); |
||||||
|
</script> |
||||||
@ -0,0 +1,55 @@ |
|||||||
|
<template> |
||||||
|
{{ display }} |
||||||
|
</template> |
||||||
|
<script setup lang="ts"> |
||||||
|
import dayjs from "dayjs"; |
||||||
|
import { computed, toRef } from "vue"; |
||||||
|
import { watchEffectOnce } from "noob-mengyxu"; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
value: string | number; |
||||||
|
valueFormat?: string; |
||||||
|
valueTz?: string; |
||||||
|
displayFormat?: string; |
||||||
|
locale?: string; |
||||||
|
type?: "iso8601" | "unix" | "unixMillis"; |
||||||
|
} |
||||||
|
|
||||||
|
const props = defineProps<Props>(); |
||||||
|
|
||||||
|
watchEffectOnce(toRef(props, "valueFormat"), async () => { |
||||||
|
const customParseFormat = await import("dayjs/plugin/customParseFormat"); |
||||||
|
dayjs.extend(customParseFormat.default); |
||||||
|
}); |
||||||
|
|
||||||
|
watchEffectOnce(toRef(props, "valueTz"), async () => { |
||||||
|
const timeZone = await import("dayjs/plugin/timezone"); |
||||||
|
dayjs.extend(timeZone.default); |
||||||
|
}); |
||||||
|
|
||||||
|
const parsed = computed(() => { |
||||||
|
if (props.type === "unix" || props.type === "unixMillis") { |
||||||
|
const value = typeof props.value === "string" ? parseFloat(props.value) : props.value; |
||||||
|
return props.type === "unix" ? dayjs.unix(value) : dayjs(value); |
||||||
|
} else { |
||||||
|
if (props.valueFormat && props.valueTz) { |
||||||
|
return dayjs.tz(props.value, props.valueFormat, props.valueTz); |
||||||
|
} else if (props.valueTz) { |
||||||
|
return dayjs.tz(props.value, props.valueTz); |
||||||
|
} else if (props.valueFormat) { |
||||||
|
return dayjs(props.value, props.valueFormat); |
||||||
|
} else { |
||||||
|
return dayjs(props.value); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const display = computed(() => { |
||||||
|
let dt = parsed.value; |
||||||
|
if (props.locale) { |
||||||
|
dt = dt.locale(props.locale); |
||||||
|
} |
||||||
|
|
||||||
|
return dt.format(props.displayFormat); |
||||||
|
}); |
||||||
|
</script> |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
|
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
|
||||||
|
</style> |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
export * from "./useActionPers"; |
||||||
|
export * from "./useListTable"; |
||||||
|
export * from "./useLoading"; |
||||||
|
export * from "./useModifyForm"; |
||||||
|
export * from "./useSysDict"; |
||||||
|
export * from "./useWatchOnce"; |
||||||
@ -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, |
||||||
|
}; |
||||||
|
} |
||||||
@ -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<any>; |
||||||
|
example: Record<string, any>; |
||||||
|
} |
||||||
|
|
||||||
|
interface Options { |
||||||
|
query: (example: any) => Promise<PageResponse<any> | Record<string, any>[] | undefined>; |
||||||
|
initialPage?: number; |
||||||
|
initialPageSize?: number; |
||||||
|
initExample?: Record<string, any>; |
||||||
|
disableAutoQuery?: boolean; |
||||||
|
deep?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export function useListTable(options: Options) { |
||||||
|
const { t } = useI18n(); |
||||||
|
const { initialPage, initialPageSize, initExample, deep } = options; |
||||||
|
|
||||||
|
type Row = Record<string, any>; |
||||||
|
type Rows = Row[] | PageResponse<Row>; |
||||||
|
|
||||||
|
const empty = <T>() => [] as T[]; |
||||||
|
const rows = deep ? ref<Rows>(empty()) : shallowRef<Rows>(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<any>({ |
||||||
|
...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, |
||||||
|
}; |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
|
} |
||||||
@ -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<any>; |
||||||
|
handleAdd?: (value) => Promise<void>; |
||||||
|
handleEdit?: (value) => Promise<void>; |
||||||
|
handleCancel?: () => Promise<void>; |
||||||
|
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<string, any>) => |
||||||
|
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<string, any> }>({ |
||||||
|
dialog: null, |
||||||
|
model: { ...initValue }, |
||||||
|
}), |
||||||
|
|
||||||
|
clearModel() { |
||||||
|
clearAndAssign(this.data.model, initValue); |
||||||
|
}, |
||||||
|
|
||||||
|
assignModel(newModel: Record<string, any>) { |
||||||
|
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<string, any>) { |
||||||
|
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(); |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
import { computed } from "vue"; |
||||||
|
import { useStore } from "vuex"; |
||||||
|
|
||||||
|
export function useSysDict() { |
||||||
|
type Callback = () => Record<string, string> | Promise<Record<string, string>>; |
||||||
|
const store = useStore(); |
||||||
|
const callbacks: Record<string, Callback> = {}; |
||||||
|
|
||||||
|
const normalize = (content: Record<string, string>) => { |
||||||
|
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<string, Callback> = {}) => { |
||||||
|
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<Record<string, Record<string, string>>>(() => store.state.dict), |
||||||
|
updateDict, |
||||||
|
unregister, |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
import { nextTick, Ref, watchEffect, WatchEffect } from "vue"; |
||||||
|
|
||||||
|
export function watchEffectOnce(cond: Ref<any, any>, cb: WatchEffect) { |
||||||
|
const stop = watchEffect((onCleanup) => { |
||||||
|
if (cond.value) { |
||||||
|
nextTick(() => stop()); |
||||||
|
cb(onCleanup); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return stop; |
||||||
|
} |
||||||
@ -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<string, any>; |
||||||
|
|
||||||
|
export interface ListTableProps { |
||||||
|
props: TableColumn[]; |
||||||
|
rowKey: string; |
||||||
|
initialPage?: number; |
||||||
|
initialPageSize?: number; |
||||||
|
initExample?: Record<string, any>; |
||||||
|
useDicts?: string[]; |
||||||
|
} |
||||||
|
|
||||||
|
interface Options { |
||||||
|
title: string; |
||||||
|
props: ListTableProps; |
||||||
|
query: (example: Record<string, any>) => Promise<PageResponse<Row>>; |
||||||
|
confirm: (rows: Row[]) => Promise<void>; |
||||||
|
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, |
||||||
|
}; |
||||||
|
} |
||||||
@ -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<string, any> | ConstructorParameters<typeof URLSearchParams>[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<QueryParams>) { |
||||||
|
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); |
||||||
|
} |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
@ -1,2 +1,4 @@ |
|||||||
export * as Axios from './axios'; |
export * as Axios from "./axios"; |
||||||
export * as Axios2 from './axios2'; |
export * as Axios2 from "./axios2"; |
||||||
|
export * as Axios3 from "./axios3"; |
||||||
|
export * from "./misc"; |
||||||
|
|||||||
@ -0,0 +1,10 @@ |
|||||||
|
export interface PageResponse<T> { |
||||||
|
data: T[]; |
||||||
|
page?: number; |
||||||
|
total: number; |
||||||
|
} |
||||||
|
|
||||||
|
export const pageEmpty = <T>(): PageResponse<T> => ({ |
||||||
|
data: [], |
||||||
|
total: 0, |
||||||
|
}); |
||||||
@ -1,7 +1,10 @@ |
|||||||
export * from './config'; |
export * from "./config"; |
||||||
export * as Element from './element'; |
export * as Element from "./element"; |
||||||
export * as Store from './store'; |
export * as Store from "./store"; |
||||||
export * as Http from './http'; |
export * as Http from "./http"; |
||||||
export * as Lang from './i18n'; |
export * as Lang from "./i18n"; |
||||||
export * as Api from './api'; |
export * as Api from "./api"; |
||||||
export * from './constant'; |
export * from "./constant"; |
||||||
|
export * from "./util/asyncUtil"; |
||||||
|
export * from "./util/objectUtil"; |
||||||
|
export * from "./composables"; |
||||||
|
|||||||
@ -0,0 +1,21 @@ |
|||||||
|
export type AsyncHandler<T> = { |
||||||
|
resolve: (t: T | PromiseLike<T>) => void; |
||||||
|
reject: (err: any) => void; |
||||||
|
}; |
||||||
|
|
||||||
|
type ExtractResult<Args extends any[]> = Args extends [AsyncHandler<infer T>, ...any[]] ? T : never; |
||||||
|
|
||||||
|
type ExtractArgs<Args extends any[]> = Args extends [AsyncHandler<any>, ...infer Rest] ? Rest : never; |
||||||
|
|
||||||
|
export function useAsyncEmits<Emits extends Record<string, any[]>>(emits: (evt: any, ...args: any[]) => void) { |
||||||
|
const emitsAsync = <Evt extends keyof Emits>(evt: Evt, ...args: ExtractArgs<Emits[Evt]>) => |
||||||
|
new Promise<ExtractResult<Emits[Evt]>>((resolve, reject) => { |
||||||
|
emits(evt, { resolve, reject }, ...args); |
||||||
|
}); |
||||||
|
|
||||||
|
return emitsAsync; |
||||||
|
} |
||||||
|
|
||||||
|
export function handleAsync<T, Args extends any[]>(handler: (...args: Args) => Promise<T>) { |
||||||
|
return ({ resolve, reject }: AsyncHandler<T>, ...args: Args) => handler(...args).then(resolve, reject); |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
import { cloneDeep } from "lodash-es"; |
||||||
|
|
||||||
|
export function deepCopy<T = any>(obj: T): T { |
||||||
|
return cloneDeep(obj); |
||||||
|
} |
||||||
|
|
||||||
|
export function clearObject(obj: Record<string, any>) { |
||||||
|
for (const key in obj) { |
||||||
|
delete obj[key]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function clearAndAssign(target: Record<string, any>, source: Record<string, any>) { |
||||||
|
clearObject(target); |
||||||
|
Object.assign(target, source); |
||||||
|
} |
||||||
|
|
||||||
|
export function unnest(obj: Record<string, any>, 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 }; |
||||||
|
} |
||||||
Loading…
Reference in new issue