20260721173401
This commit is contained in:
1 parent
fd161dd302
commit
bab8e2972f
32 files changed
+1207
-3171
No files matched your search
@@ -1,197 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
bordered: { type: Boolean, default: true },
|
||||
scroll: { type: Object, default: () => ({}) },
|
||||
scrollX: { type: [Boolean, Number, String], default: "max-content" },
|
||||
});
|
||||
|
||||
const tableAreaRef = ref(null);
|
||||
const bodyHeight = ref(240);
|
||||
const mergedScroll = computed(() => ({
|
||||
...props.scroll,
|
||||
x: props.scrollX,
|
||||
y: bodyHeight.value,
|
||||
}));
|
||||
|
||||
let resizeObserver;
|
||||
let mutationObserver;
|
||||
let frameId = 0;
|
||||
const observedElements = new Set();
|
||||
|
||||
function getOuterHeight(element) {
|
||||
if (!element) return 0;
|
||||
|
||||
const style = getComputedStyle(element);
|
||||
const marginTop = Number.parseFloat(style.marginTop) || 0;
|
||||
const marginBottom = Number.parseFloat(style.marginBottom) || 0;
|
||||
|
||||
return element.getBoundingClientRect().height + marginTop + marginBottom;
|
||||
}
|
||||
|
||||
function clipsOverflowY(element) {
|
||||
const { overflow, overflowY } = getComputedStyle(element);
|
||||
return [overflow, overflowY].some((value) => /^(auto|clip|hidden|scroll)$/.test(value));
|
||||
}
|
||||
|
||||
function getAvailableHeight(area) {
|
||||
const areaTop = area.getBoundingClientRect().top;
|
||||
let boundaryBottom = Number.POSITIVE_INFINITY;
|
||||
let ancestor = area.parentElement;
|
||||
|
||||
while (ancestor && ancestor !== document.documentElement) {
|
||||
if (clipsOverflowY(ancestor)) {
|
||||
const rect = ancestor.getBoundingClientRect();
|
||||
boundaryBottom = Math.min(boundaryBottom, rect.top + ancestor.clientTop + ancestor.clientHeight);
|
||||
}
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
|
||||
if (Number.isFinite(boundaryBottom) && boundaryBottom > areaTop) {
|
||||
return Math.floor(boundaryBottom - areaTop);
|
||||
}
|
||||
return area.clientHeight;
|
||||
}
|
||||
|
||||
function updateBodyHeight() {
|
||||
const area = tableAreaRef.value;
|
||||
if (!area?.clientHeight) return;
|
||||
|
||||
const headerHeight = getOuterHeight(
|
||||
area.querySelector(".ant-table-header") || area.querySelector(".ant-table-thead"),
|
||||
);
|
||||
const paginationHeight = getOuterHeight(area.querySelector(".ant-table-pagination"));
|
||||
const titleHeight = getOuterHeight(area.querySelector(".ant-table-title"));
|
||||
const footerHeight = getOuterHeight(area.querySelector(".ant-table-footer"));
|
||||
|
||||
const nextHeight = Math.max(
|
||||
1,
|
||||
getAvailableHeight(area) - headerHeight - paginationHeight - titleHeight - footerHeight,
|
||||
);
|
||||
if (nextHeight !== bodyHeight.value) bodyHeight.value = nextHeight;
|
||||
}
|
||||
|
||||
function scheduleHeightUpdate() {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = requestAnimationFrame(updateBodyHeight);
|
||||
}
|
||||
|
||||
function observe(element) {
|
||||
if (!element || observedElements.has(element)) return;
|
||||
resizeObserver?.observe(element);
|
||||
observedElements.add(element);
|
||||
}
|
||||
|
||||
function syncObservedElements() {
|
||||
const area = tableAreaRef.value;
|
||||
if (!area) return;
|
||||
|
||||
let ancestor = area.parentElement;
|
||||
while (ancestor && ancestor !== document.documentElement) {
|
||||
if (ancestor === area.parentElement || clipsOverflowY(ancestor)) observe(ancestor);
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
observe(area.querySelector(".ant-table-header"));
|
||||
observe(area.querySelector(".ant-table-pagination"));
|
||||
observe(area.querySelector(".ant-table-title"));
|
||||
observe(area.querySelector(".ant-table-footer"));
|
||||
scheduleHeightUpdate();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
resizeObserver = new ResizeObserver(scheduleHeightUpdate);
|
||||
mutationObserver = new MutationObserver(() => {
|
||||
nextTick(syncObservedElements);
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
syncObservedElements();
|
||||
mutationObserver.observe(tableAreaRef.value, { childList: true, subtree: true });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cancelAnimationFrame(frameId);
|
||||
resizeObserver?.disconnect();
|
||||
mutationObserver?.disconnect();
|
||||
observedElements.clear();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="tableAreaRef"
|
||||
class="auto-height-table"
|
||||
:style="{ '--auto-table-body-height': bodyHeight + 'px' }"
|
||||
>
|
||||
<a-table v-bind="$attrs" :bordered="bordered" :scroll="mergedScroll" size="small">
|
||||
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps || {}" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auto-height-table {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-table-wrapper),
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container),
|
||||
:deep(.ant-table),
|
||||
:deep(.ant-table-container) {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-table-wrapper) {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.ant-spin-nested-loading),
|
||||
:deep(.ant-spin-container),
|
||||
:deep(.ant-table),
|
||||
:deep(.ant-table-container) {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.ant-table-header),
|
||||
:deep(.ant-table-pagination) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
:deep(.ant-table-body) {
|
||||
height: var(--auto-table-body-height);
|
||||
min-height: 0;
|
||||
max-height: var(--auto-table-body-height) !important;
|
||||
flex: 0 0 var(--auto-table-body-height);
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* scroll.y 模式下 antdv 不会渲染 body 底部外边框;且直接加在滚动容器 .ant-table-body
|
||||
上时,边框会被内部滚动内容的最后一行盖住、或被祖先 overflow:hidden 裁掉,表现为
|
||||
“看不到/被遮挡”。所以把底边框挂在不滚动的 .ant-table-container 上,并强制 border-box
|
||||
让边框计入盒子高度,避免溢出裁切。 */
|
||||
::deep(.ant-table-container),
|
||||
::deep(.ant-table-body) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::deep(.ant-table-bordered .ant-table-container) {
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary, #f0f0f0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useLookupOptions } from "./select/useLookupOptions";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: "" },
|
||||
labelKey: { type: String, default: "b_name" },
|
||||
valueKey: { type: String, default: "b_id" },
|
||||
viewName: { type: String, required: true },
|
||||
searchCondition: { type: String, default: "" },
|
||||
searchColumns: { type: Array, default: () => [] },
|
||||
orderBy: { type: String, default: "" },
|
||||
pageSize: { type: Number, default: 20 },
|
||||
searchDelay: { type: Number, default: 300 },
|
||||
allowClear: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
const lookup = useLookupOptions(props);
|
||||
|
||||
const options = computed(() =>
|
||||
lookup.rows.value.map((row) => ({
|
||||
value: String(row[props.labelKey] ?? ""),
|
||||
label: String(row[props.labelKey] ?? ""),
|
||||
key: String(row[props.valueKey] ?? row[props.labelKey] ?? ""),
|
||||
record: row,
|
||||
})),
|
||||
);
|
||||
|
||||
function onOpenChange(open) {
|
||||
if (open) lookup.open();
|
||||
else lookup.resetAfterClose();
|
||||
}
|
||||
|
||||
function onChange(value) {
|
||||
emit("update:modelValue", String(value ?? ""));
|
||||
if (!value) emit("change", null);
|
||||
}
|
||||
|
||||
function onSelect(_value, option) {
|
||||
emit("change", option.record ?? null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-auto-complete
|
||||
v-bind="$attrs"
|
||||
class="fms-auto-complete"
|
||||
:value="modelValue"
|
||||
:options="options"
|
||||
:show-search="{ onSearch: lookup.search }"
|
||||
:not-found-content="lookup.loading.value ? '加载中...' : '暂无数据'"
|
||||
:allow-clear="allowClear"
|
||||
@change="onChange"
|
||||
@focus="lookup.open"
|
||||
@open-change="onOpenChange"
|
||||
@popup-scroll="lookup.onPopupScroll"
|
||||
@select="onSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fms-auto-complete {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: "" },
|
||||
disabled: { type: Boolean, default: false },
|
||||
readonly: { type: Boolean, default: false },
|
||||
allowClear: { type: Boolean, default: true },
|
||||
maxlength: { type: Number, default: undefined },
|
||||
popupWidth: { type: [Number, String], default: 420 },
|
||||
popupHeight: { type: [Number, String], default: 180 },
|
||||
placement: { type: String, default: "bottomCenter" },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change", "focus", "blur", "openChange"]);
|
||||
|
||||
const open = ref(false);
|
||||
const inputRef = ref(null);
|
||||
const textareaRef = ref(null);
|
||||
const editorRef = ref(null);
|
||||
const triggerWidth = ref(0);
|
||||
|
||||
const popoverPlacement = computed(() =>
|
||||
props.placement === "bottomCenter" ? "bottom" : props.placement,
|
||||
);
|
||||
const popupWidth = computed(() =>
|
||||
typeof props.popupWidth === "number" ? `${props.popupWidth}px` : props.popupWidth,
|
||||
);
|
||||
const popupHeight = computed(() =>
|
||||
typeof props.popupHeight === "number" ? `${props.popupHeight}px` : props.popupHeight,
|
||||
);
|
||||
const popupStyles = computed(() => ({
|
||||
container: {
|
||||
width: popupWidth.value,
|
||||
minWidth: triggerWidth.value ? `${triggerWidth.value}px` : undefined,
|
||||
maxWidth: "calc(100vw - 24px)",
|
||||
padding: "8px",
|
||||
},
|
||||
}));
|
||||
|
||||
function updateValue(value) {
|
||||
emit("update:modelValue", value ?? "");
|
||||
}
|
||||
|
||||
function onInputChange(event) {
|
||||
emit("change", event);
|
||||
}
|
||||
|
||||
async function showEditor() {
|
||||
if (props.disabled || open.value) return;
|
||||
|
||||
const inputElement = inputRef.value?.$el ?? inputRef.value;
|
||||
triggerWidth.value = inputElement?.getBoundingClientRect?.().width ?? 0;
|
||||
open.value = true;
|
||||
emit("openChange", true);
|
||||
await nextTick();
|
||||
textareaRef.value?.focus?.({ cursor: "end" });
|
||||
}
|
||||
|
||||
function onInputFocus(event) {
|
||||
emit("focus", event);
|
||||
showEditor();
|
||||
}
|
||||
|
||||
function onOpenChange(value) {
|
||||
if (value && props.disabled) return;
|
||||
open.value = value;
|
||||
emit("openChange", value);
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
if (!open.value) return;
|
||||
open.value = false;
|
||||
emit("openChange", false);
|
||||
}
|
||||
|
||||
function onDocumentPointerDown(event) {
|
||||
if (!open.value) return;
|
||||
|
||||
const inputElement = inputRef.value?.$el ?? inputRef.value;
|
||||
const path = event.composedPath?.() ?? [];
|
||||
if (path.includes(inputElement) || path.includes(editorRef.value)) return;
|
||||
closeEditor();
|
||||
}
|
||||
|
||||
function onTextareaKeydown(event) {
|
||||
event.stopPropagation();
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeEditor();
|
||||
}
|
||||
}
|
||||
|
||||
function onInputBlur(event) {
|
||||
if (!open.value) emit("blur", event);
|
||||
}
|
||||
|
||||
function onTextareaBlur(event) {
|
||||
emit("blur", event);
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener("pointerdown", onDocumentPointerDown, true));
|
||||
onBeforeUnmount(() => document.removeEventListener("pointerdown", onDocumentPointerDown, true));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-popover
|
||||
:open="open"
|
||||
:trigger="[]"
|
||||
:placement="popoverPlacement"
|
||||
:arrow="false"
|
||||
:styles="popupStyles"
|
||||
@open-change="onOpenChange"
|
||||
>
|
||||
<template #content>
|
||||
<div ref="editorRef" class="fms-memo-editor" @mousedown.stop @click.stop>
|
||||
<a-textarea
|
||||
ref="textareaRef"
|
||||
:value="modelValue"
|
||||
:maxlength="maxlength"
|
||||
:placeholder="$attrs.placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:style="{ height: popupHeight, resize: 'both' }"
|
||||
@update:value="updateValue"
|
||||
@change="onInputChange"
|
||||
@keydown="onTextareaKeydown"
|
||||
@blur="onTextareaBlur"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-input
|
||||
ref="inputRef"
|
||||
v-bind="$attrs"
|
||||
class="fms-memo-input"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:allow-clear="allowClear"
|
||||
:maxlength="maxlength"
|
||||
@update:value="updateValue"
|
||||
@change="onInputChange"
|
||||
@focus="onInputFocus"
|
||||
@blur="onInputBlur"
|
||||
@click.stop="showEditor"
|
||||
@keydown.stop
|
||||
>
|
||||
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps || {}" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fms-memo-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fms-memo-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fms-memo-editor :deep(textarea) {
|
||||
min-width: 180px;
|
||||
min-height: 96px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useLookupOptions } from "./select/useLookupOptions";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: null, default: "" },
|
||||
label: { type: String, default: "" },
|
||||
labelKey: { type: String, default: "b_name" },
|
||||
valueKey: { type: String, default: "b_id" },
|
||||
viewName: { type: String, required: true },
|
||||
searchCondition: { type: String, default: "" },
|
||||
searchColumns: { type: Array, default: () => [] },
|
||||
orderBy: { type: String, default: "" },
|
||||
pageSize: { type: Number, default: 20 },
|
||||
searchDelay: { type: Number, default: 300 },
|
||||
allowClear: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "update:label", "change"]);
|
||||
const lookup = useLookupOptions(props);
|
||||
|
||||
const options = computed(() => {
|
||||
const items = lookup.rows.value.map((row) => ({
|
||||
value: row[props.valueKey],
|
||||
label: String(row[props.labelKey] ?? ""),
|
||||
record: row,
|
||||
}));
|
||||
|
||||
if (
|
||||
props.modelValue !== "" &&
|
||||
props.modelValue != null &&
|
||||
props.label &&
|
||||
!items.some((item) => item.value === props.modelValue)
|
||||
) {
|
||||
items.unshift({ value: props.modelValue, label: props.label, record: null });
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
function onOpenChange(open) {
|
||||
if (open) lookup.open();
|
||||
else lookup.resetAfterClose();
|
||||
}
|
||||
|
||||
function onSelect(value, option) {
|
||||
emit("update:modelValue", value);
|
||||
emit("update:label", String(option.label ?? ""));
|
||||
emit("change", option.record ?? null);
|
||||
}
|
||||
|
||||
function onChange(value) {
|
||||
if (value !== undefined && value !== null) return;
|
||||
emit("update:modelValue", "");
|
||||
emit("update:label", "");
|
||||
emit("change", null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-select
|
||||
v-bind="$attrs"
|
||||
class="fms-select"
|
||||
:value="modelValue === '' ? undefined : modelValue"
|
||||
:options="options"
|
||||
:loading="lookup.loading.value"
|
||||
:filter-option="false"
|
||||
:allow-clear="allowClear"
|
||||
show-search
|
||||
@search="lookup.search"
|
||||
@open-change="onOpenChange"
|
||||
@popup-scroll="lookup.onPopupScroll"
|
||||
@select="onSelect"
|
||||
@change="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fms-select {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, useSlots } from "vue";
|
||||
import { useRafFn, useResizeObserver } from "@vueuse/core";
|
||||
import { StkTable } from "stk-table-vue";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
defineProps({
|
||||
virtual: { type: Boolean, default: true },
|
||||
virtualX: { type: Boolean, default: true },
|
||||
colResizable: { type: Boolean, default: true },
|
||||
bordered: { type: [Boolean, String], default: true },
|
||||
noDataFull: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const tableRef = ref(null);
|
||||
|
||||
const { resume: refreshLayout } = useRafFn(
|
||||
async () => {
|
||||
await nextTick();
|
||||
tableRef.value?.initVirtualScroll?.();
|
||||
await nextTick();
|
||||
},
|
||||
{ immediate: false, once: true },
|
||||
);
|
||||
|
||||
useResizeObserver(
|
||||
() => tableRef.value?.$el,
|
||||
([entry]) => {
|
||||
const { width, height } = entry.contentRect;
|
||||
if (width > 0 && height > 0) refreshLayout();
|
||||
},
|
||||
);
|
||||
|
||||
// Forward every slot except `empty`; the `empty` slot is handled below so the
|
||||
// default Result fallback only renders when the parent does not provide one.
|
||||
const forwardSlots = computed(() => {
|
||||
const result = {};
|
||||
for (const name in slots) {
|
||||
if (name !== "empty") result[name] = slots[name];
|
||||
}
|
||||
return result;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StkTable
|
||||
ref="tableRef"
|
||||
class="fms-table"
|
||||
v-bind="$attrs"
|
||||
:auto-resize="false"
|
||||
:virtual="virtual"
|
||||
:virtual-x="virtualX"
|
||||
:col-resizable="colResizable"
|
||||
:bordered="bordered"
|
||||
:no-data-full="noDataFull"
|
||||
>
|
||||
<template v-for="(slot, name) in forwardSlots" :key="name" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps || {}" />
|
||||
</template>
|
||||
<template v-if="slots.empty" #empty>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
<template v-else #empty>
|
||||
<a-result status="info" title="暂无数据" sub-title="当前没有可显示的内容" />
|
||||
</template>
|
||||
</StkTable>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fms-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
defineProps({
|
||||
blockNode: { type: Boolean, default: true },
|
||||
virtual: { type: Boolean, default: true },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-tree v-bind="$attrs" :block-node="blockNode" :virtual="virtual">
|
||||
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps || {}" />
|
||||
</template>
|
||||
</a-tree>
|
||||
</template>
|
||||
@@ -0,0 +1,128 @@
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useTimeoutFn } from "@vueuse/core";
|
||||
import { pageDataApi } from "@/services/api";
|
||||
|
||||
function escapeSearchText(value) {
|
||||
return String(value).replaceAll("'", "''");
|
||||
}
|
||||
|
||||
function combineConditions(baseCondition, searchCondition) {
|
||||
if (!baseCondition) return searchCondition;
|
||||
if (!searchCondition) return baseCondition;
|
||||
return `(${baseCondition}) AND (${searchCondition})`;
|
||||
}
|
||||
|
||||
export function useLookupOptions(props) {
|
||||
const rows = ref([]);
|
||||
const loading = ref(false);
|
||||
const pageNo = ref(0);
|
||||
const total = ref(0);
|
||||
const query = ref("");
|
||||
|
||||
const searchColumns = computed(() => [
|
||||
...new Set([props.labelKey, props.valueKey, ...props.searchColumns].filter(Boolean)),
|
||||
]);
|
||||
|
||||
let requestId = 0;
|
||||
|
||||
async function load(searchText = query.value, append = false) {
|
||||
if (!props.viewName) return;
|
||||
|
||||
const currentRequestId = ++requestId;
|
||||
const requestedPage = append ? pageNo.value + 1 : 1;
|
||||
const normalizedSearchText = String(searchText ?? "").trim();
|
||||
const searchFilter = normalizedSearchText
|
||||
? `${props.labelKey} LIKE '%${escapeSearchText(normalizedSearchText)}%'`
|
||||
: "";
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await pageDataApi({
|
||||
viewName: props.viewName,
|
||||
orderBy: props.orderBy || props.labelKey,
|
||||
searchColumns: searchColumns.value,
|
||||
searchCondition: combineConditions(props.searchCondition, searchFilter),
|
||||
pageNo: requestedPage,
|
||||
pageSize: props.pageSize,
|
||||
});
|
||||
|
||||
if (currentRequestId !== requestId) return;
|
||||
|
||||
const result = response.data ?? {};
|
||||
const nextRows = Array.isArray(result.rows) ? result.rows : [];
|
||||
rows.value = append ? [...rows.value, ...nextRows] : nextRows;
|
||||
pageNo.value = Number(result.page_no) || requestedPage;
|
||||
total.value = Number(result.total ?? result.row_count) || 0;
|
||||
query.value = normalizedSearchText;
|
||||
} catch {
|
||||
if (currentRequestId === requestId && !append) {
|
||||
rows.value = [];
|
||||
pageNo.value = 0;
|
||||
total.value = 0;
|
||||
}
|
||||
} finally {
|
||||
if (currentRequestId === requestId) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const { start: startSearchTimer, stop: stopSearchTimer } = useTimeoutFn(
|
||||
() => load(query.value),
|
||||
() => props.searchDelay,
|
||||
{ immediate: false },
|
||||
);
|
||||
const { start: startResetTimer, stop: stopResetTimer } = useTimeoutFn(reset, 200, {
|
||||
immediate: false,
|
||||
});
|
||||
|
||||
function search(value) {
|
||||
stopResetTimer();
|
||||
query.value = String(value ?? "");
|
||||
startSearchTimer();
|
||||
}
|
||||
|
||||
function open() {
|
||||
stopResetTimer();
|
||||
if (!rows.value.length && !loading.value) load(query.value);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
stopSearchTimer();
|
||||
stopResetTimer();
|
||||
requestId += 1;
|
||||
rows.value = [];
|
||||
loading.value = false;
|
||||
pageNo.value = 0;
|
||||
total.value = 0;
|
||||
query.value = "";
|
||||
}
|
||||
|
||||
function resetAfterClose() {
|
||||
startResetTimer();
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (loading.value || rows.value.length >= total.value) return;
|
||||
load(query.value, true);
|
||||
}
|
||||
|
||||
function onPopupScroll(event) {
|
||||
const target = event.currentTarget || event.target;
|
||||
if (target.scrollTop + target.clientHeight >= target.scrollHeight - 24) loadMore();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
props.viewName,
|
||||
props.labelKey,
|
||||
props.valueKey,
|
||||
props.orderBy,
|
||||
props.searchCondition,
|
||||
props.searchColumns,
|
||||
],
|
||||
reset,
|
||||
);
|
||||
|
||||
onBeforeUnmount(reset);
|
||||
|
||||
return { rows, loading, query, load, search, open, resetAfterClose, onPopupScroll };
|
||||
}
|
||||
Reference in new issue
Block a user