20260730231110

This commit is contained in:
oneao committed 2026-07-30 23:11:10 +08:00
1 parent 4929b78434
commit 318fa0d9e1
23 files changed
+4026 -506

No files matched your search

+105 -31
View File
@@ -1,5 +1,5 @@
<script setup>
import { computed } from "vue";
import { computed, ref, watch } from "vue";
import { useLookupOptions } from "./select/useLookupOptions";
defineOptions({ inheritAttrs: false });
@@ -15,69 +15,143 @@ const props = defineProps({
orderBy: { type: String, default: "" },
pageSize: { type: Number, default: 20 },
searchDelay: { type: Number, default: 300 },
allowClear: { type: Boolean, default: true },
allowClear: { type: Boolean, default: false },
});
const emit = defineEmits(["update:modelValue", "update:label", "change"]);
const lookup = useLookupOptions(props);
const inputValue = ref("");
const clearedByInput = ref(false);
const options = computed(() => {
const items = lookup.rows.value.map((row) => ({
value: row[props.valueKey],
label: String(row[props.labelKey] ?? ""),
record: row,
}));
const items = lookup.rows.value.map((row) => {
const label = String(row[props.labelKey] ?? "");
const value = row[props.valueKey];
return {
value: label,
label,
key: String(value ?? label),
rawValue: value,
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 });
if (props.modelValue !== "" && props.modelValue != null && props.label) {
const exists = items.some((item) => String(item.rawValue) === String(props.modelValue));
if (!exists) {
items.unshift({
value: props.label,
label: props.label,
key: String(props.modelValue),
rawValue: props.modelValue,
record: null,
});
}
}
return items;
});
function onOpenChange(open) {
if (open) lookup.open();
else lookup.resetAfterClose();
watch(
() => [props.modelValue, props.label],
() => {
if (props.modelValue === "" || props.modelValue == null) {
if (!props.label) inputValue.value = "";
return;
}
clearedByInput.value = false;
inputValue.value = props.label || "";
},
{ immediate: true },
);
function onSearch(value) {
inputValue.value = String(value ?? "");
if (!inputValue.value) {
clearSelection();
return;
}
lookup.search(value);
}
function onSelect(value, option) {
emit("update:modelValue", value);
emit("update:label", String(option.label ?? ""));
emit("change", option.record ?? null);
function onInputChange(value) {
inputValue.value = String(value ?? "");
if (!inputValue.value) {
clearSelection();
return;
}
clearedByInput.value = false;
}
function onChange(value) {
if (value !== undefined && value !== null) return;
function clearSelection() {
inputValue.value = "";
clearedByInput.value = true;
emit("update:modelValue", "");
emit("update:label", "");
emit("change", null);
}
function onBlur() {
if (clearedByInput.value || !inputValue.value) return;
inputValue.value = props.label || "";
}
function onOpenChange(open) {
if (open) lookup.open();
else {
lookup.resetAfterClose();
if (clearedByInput.value || !inputValue.value) return;
inputValue.value = props.label || "";
}
}
function onSelect(_value, option) {
const record = option.record ?? null;
const value = record ? record[props.valueKey] : option.rawValue;
const label = record ? String(record[props.labelKey] ?? "") : String(option.label ?? "");
inputValue.value = label;
clearedByInput.value = false;
emit("update:modelValue", value ?? "");
emit("update:label", label);
emit("change", record);
}
function onInputKeydown(event) {
if (event.key !== "Backspace" && event.key !== "Delete") return;
const checkCleared = () => {
const target = event.target;
const value = target && "value" in target ? String(target.value ?? "") : inputValue.value;
if (!value) clearSelection();
};
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(checkCleared);
} else {
setTimeout(checkCleared, 0);
}
}
</script>
<template>
<a-select
<a-auto-complete
v-bind="$attrs"
class="fms-select"
:value="modelValue === '' ? undefined : modelValue"
:value="inputValue"
:options="options"
:loading="lookup.loading.value"
:filter-option="false"
:show-search="{ onSearch }"
:not-found-content="lookup.loading.value ? '加载中...' : '暂无数据'"
:allow-clear="allowClear"
show-search
@search="lookup.search"
@change="onInputChange"
@clear="clearSelection"
@blur="onBlur"
@focus="lookup.open"
@input-keydown="onInputKeydown"
@open-change="onOpenChange"
@popup-scroll="lookup.onPopupScroll"
@select="onSelect"
@change="onChange"
/>
</template>
<style scoped>
<style scoped lang="scss">
.fms-select {
width: 100%;
}
+21 -3
View File
@@ -1,9 +1,11 @@
<script setup>
import { computed, ref, useAttrs, watch } from "vue";
import { useAppStore } from "@/stores/app";
defineOptions({ inheritAttrs: false });
const attrs = useAttrs();
const appStore = useAppStore();
const handledEventAttrs = new Set([
"onDrop",
"onDragStart",
@@ -36,7 +38,6 @@ const props = defineProps({
draggable: { type: [Boolean, Function, Object], default: false },
allowDrop: { type: Function, default: null },
rootKey: { type: [String, Number], default: "__fms_tree_root__" },
rootTitle: { type: String, default: "全部分类" },
// 右键菜单生成函数:(node) => { items: [...] }
contextMenu: { type: Function, default: null },
contextMenuDisabled: { type: Boolean, default: false },
@@ -48,13 +49,30 @@ const effectiveVirtual = computed(() => {
return props.virtual;
});
function getNodeTitle(node) {
const fallback = node?.title || node?.b_name || node?.b_code || "";
return appStore.t(node?.b_i18n, fallback);
}
function localizeNodes(nodes) {
const result = [];
for (const node of nodes || []) {
result.push({
...node,
title: getNodeTitle(node),
children: localizeNodes(node.children),
});
}
return result;
}
const effectiveTreeData = computed(() => {
return [
{
key: props.rootKey,
title: props.rootTitle,
title: appStore.t("common.allCategories", "全部分类"),
isFmsRoot: true,
children: props.treeData,
children: localizeNodes(props.treeData),
},
];
});
@@ -1,4 +1,4 @@
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { onBeforeUnmount, ref, watch } from "vue";
import { useTimeoutFn } from "@vueuse/core";
import { pageDataApi } from "@/services/api";
@@ -19,10 +19,6 @@ export function useLookupOptions(props) {
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) {
@@ -40,7 +36,7 @@ export function useLookupOptions(props) {
const response = await pageDataApi({
viewName: props.viewName,
orderBy: props.orderBy || props.labelKey,
searchColumns: searchColumns.value,
searchColumns: props.searchColumns.length ? props.searchColumns : undefined,
searchCondition: combineConditions(props.searchCondition, searchFilter),
pageNo: requestedPage,
pageSize: props.pageSize,