20260728173026
This commit is contained in:
1 parent
63588bd217
commit
5ec7aadef9
25 files changed
+627
-462
No files matched your search
@@ -1,16 +1,71 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
blockNode: { type: Boolean, default: true },
|
||||
virtual: { type: Boolean, default: true },
|
||||
// 右键菜单生成函数:(node) => { items: [...] }
|
||||
contextMenu: { type: Function, default: null },
|
||||
contextMenuDisabled: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["menuClick"]);
|
||||
|
||||
const contextNode = ref(null);
|
||||
|
||||
function onTreeRightClick({ node }) {
|
||||
contextNode.value = node;
|
||||
}
|
||||
|
||||
const contextMenuItems = computed(() => {
|
||||
if (!props.contextMenu || !contextNode.value) return undefined;
|
||||
const result = props.contextMenu(contextNode.value);
|
||||
if (!result) return undefined;
|
||||
return { ...result, onClick: onMenuClick };
|
||||
});
|
||||
|
||||
function onMenuClick({ key }) {
|
||||
const node = contextNode.value;
|
||||
contextNode.value = null;
|
||||
if (node) emit("menuClick", { key, node });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-tree v-bind="$attrs" :block-node="blockNode" :virtual="virtual">
|
||||
<a-dropdown
|
||||
v-if="contextMenu"
|
||||
:trigger="['contextmenu']"
|
||||
:menu="contextMenuItems"
|
||||
:disabled="contextMenuDisabled"
|
||||
>
|
||||
<div class="fms-tree-wrapper">
|
||||
<a-tree
|
||||
v-bind="$attrs"
|
||||
:block-node="blockNode"
|
||||
:virtual="virtual"
|
||||
@right-click="onTreeRightClick"
|
||||
>
|
||||
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps || {}" />
|
||||
</template>
|
||||
</a-tree>
|
||||
</div>
|
||||
</a-dropdown>
|
||||
<a-tree v-else 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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fms-tree-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, h, ref, shallowRef, useSlots, watch } from "vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { formatDateValue, parseOptions } from "./moduleListUtils";
|
||||
|
||||
const props = defineProps({
|
||||
fields: { type: Array, default: () => [] },
|
||||
@@ -31,52 +32,14 @@ const expansionOptions = computed(() =>
|
||||
);
|
||||
const expandConfig = computed(() => ({ height: Number(expansionOptions.value.height) || 180 }));
|
||||
|
||||
function parseOptions(field) {
|
||||
if (!field?.b_options) return { config: {}, options: [] };
|
||||
try {
|
||||
const parsed = JSON.parse(field.b_options);
|
||||
const config = Array.isArray(parsed) ? {} : parsed || {};
|
||||
const source = Array.isArray(parsed)
|
||||
? parsed
|
||||
: Array.isArray(parsed?.options)
|
||||
? parsed.options
|
||||
: Array.isArray(parsed?.items)
|
||||
? parsed.items
|
||||
: [];
|
||||
const options = source.map((option) => {
|
||||
if (option && typeof option === "object") {
|
||||
const value = option.value ?? option.key ?? option.id;
|
||||
return { ...option, value, label: String(option.label ?? option.name ?? value ?? "") };
|
||||
}
|
||||
return { value: option, label: String(option ?? "") };
|
||||
});
|
||||
return { config, options };
|
||||
} catch {
|
||||
return { config: {}, options: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateValue(value, withTime) {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
const text = String(value).replace("T", " ");
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(text)) return withTime ? text.slice(0, 19) : text.slice(0, 10);
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return text;
|
||||
const pad = (number) => String(number).padStart(2, "0");
|
||||
const datePart = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
return withTime
|
||||
? `${datePart} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
: datePart;
|
||||
}
|
||||
|
||||
function formatCellValue(value, field, optionInfo) {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
if (field.b_type === "date") return formatDateValue(value, false);
|
||||
if (field.b_type === "datetime") return formatDateValue(value, true);
|
||||
if (field.b_type === "switch") {
|
||||
return value === true || Number(value) === 1
|
||||
? (optionInfo.config.trueLabel ?? "是")
|
||||
: (optionInfo.config.falseLabel ?? "否");
|
||||
? (optionInfo.optionConfig.trueLabel ?? "是")
|
||||
: (optionInfo.optionConfig.falseLabel ?? "否");
|
||||
}
|
||||
if (field.b_type === "select") {
|
||||
const option = optionInfo.options.find((item) => String(item.value) === String(value));
|
||||
@@ -85,8 +48,8 @@ function formatCellValue(value, field, optionInfo) {
|
||||
if (field.b_type === "money") {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) return String(value);
|
||||
const precision = Number.isInteger(optionInfo.config.precision)
|
||||
? optionInfo.config.precision
|
||||
const precision = Number.isInteger(optionInfo.optionConfig.precision)
|
||||
? optionInfo.optionConfig.precision
|
||||
: 2;
|
||||
return number.toLocaleString("zh-CN", {
|
||||
minimumFractionDigits: precision,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { isDateQuery, isEmptyValue, isNumericField, isRangeQuery } from "./moduleListUtils";
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
@@ -9,21 +10,9 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "enter"]);
|
||||
|
||||
const isRange = computed(
|
||||
() =>
|
||||
props.item.config.b_operator === "between" ||
|
||||
props.item.config.b_component === "number-range" ||
|
||||
props.item.field.b_type === "datetime",
|
||||
);
|
||||
const isNumeric = computed(
|
||||
() => props.item.field.b_type === "number" || props.item.field.b_type === "money",
|
||||
);
|
||||
const isDate = computed(
|
||||
() =>
|
||||
props.item.config.b_component === "date-picker" ||
|
||||
props.item.field.b_type === "date" ||
|
||||
props.item.field.b_type === "datetime",
|
||||
);
|
||||
const isRange = computed(() => isRangeQuery(props.item));
|
||||
const isNumeric = computed(() => isNumericField(props.item));
|
||||
const isDate = computed(() => isDateQuery(props.item));
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (nextValue) => emit("update:modelValue", nextValue),
|
||||
@@ -45,10 +34,6 @@ const endValue = computed({
|
||||
},
|
||||
});
|
||||
|
||||
function isEmptyValue(current) {
|
||||
return current === undefined || current === null || current === "";
|
||||
}
|
||||
|
||||
function comparableDate(current) {
|
||||
if (isEmptyValue(current)) return null;
|
||||
if (current && typeof current.format === "function") {
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ChevronDown, ChevronUp, Ellipsis, RotateCcw, Search } from "@lucide/vue";
|
||||
import FmsQueryControl from "./FmsQueryControl.vue";
|
||||
import {
|
||||
defaultQueryValue,
|
||||
emptyQueryValue,
|
||||
isDateQuery,
|
||||
isNumericField,
|
||||
isRangeQuery,
|
||||
} from "./moduleListUtils";
|
||||
|
||||
const props = defineProps({
|
||||
queryItems: { type: Array, default: () => [] },
|
||||
@@ -35,55 +42,6 @@ const visibleAdvancedQueryItems = computed(() =>
|
||||
advancedMoreOpen.value ? props.queryItems : props.queryItems.slice(0, props.defaultAdvancedCount),
|
||||
);
|
||||
|
||||
function isRangeQuery(item) {
|
||||
return (
|
||||
item.config.b_operator === "between" ||
|
||||
item.config.b_component === "number-range" ||
|
||||
item.field.b_type === "datetime"
|
||||
);
|
||||
}
|
||||
|
||||
function isNumericField(item) {
|
||||
return item.field.b_type === "number" || item.field.b_type === "money";
|
||||
}
|
||||
|
||||
function isDateQuery(item) {
|
||||
return (
|
||||
item.config.b_component === "date-picker" ||
|
||||
item.field.b_type === "date" ||
|
||||
item.field.b_type === "datetime"
|
||||
);
|
||||
}
|
||||
|
||||
function emptyQueryValue(item) {
|
||||
if (isRangeQuery(item) || item.config.b_operator === "in") return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function defaultQueryValue(item) {
|
||||
const configuredValue = item.config.b_default_value;
|
||||
if (configuredValue === null || configuredValue === undefined || configuredValue === "") {
|
||||
return emptyQueryValue(item);
|
||||
}
|
||||
if (isRangeQuery(item) || item.config.b_operator === "in") {
|
||||
try {
|
||||
const parsed = JSON.parse(configuredValue);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
} catch {
|
||||
// 非 JSON 默认值继续按逗号分隔处理。
|
||||
}
|
||||
return String(configuredValue)
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (isNumericField(item)) {
|
||||
const number = Number(configuredValue);
|
||||
return Number.isFinite(number) ? number : undefined;
|
||||
}
|
||||
return configuredValue;
|
||||
}
|
||||
|
||||
function cloneQueryValue(current) {
|
||||
return Array.isArray(current) ? [...current] : current;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// 数据驱动列表的共享工具函数,避免在各组件间重复实现查询/格式化逻辑。
|
||||
|
||||
export const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
|
||||
export function escapeSqlText(value) {
|
||||
return String(value).replaceAll("'", "''");
|
||||
}
|
||||
|
||||
export function requireModuleId(value) {
|
||||
const id = String(value ?? "");
|
||||
if (!/^\d+$/.test(id)) throw new Error("模块 ID 格式不正确");
|
||||
return id;
|
||||
}
|
||||
|
||||
export function isEmptyValue(value) {
|
||||
return value === undefined || value === null || value === "";
|
||||
}
|
||||
|
||||
export function parseOptions(field) {
|
||||
if (!field?.b_options) return { optionConfig: {}, options: [] };
|
||||
try {
|
||||
const parsed = JSON.parse(field.b_options);
|
||||
const optionConfig = Array.isArray(parsed) ? {} : parsed || {};
|
||||
const source = Array.isArray(parsed)
|
||||
? parsed
|
||||
: Array.isArray(parsed?.options)
|
||||
? parsed.options
|
||||
: Array.isArray(parsed?.items)
|
||||
? parsed.items
|
||||
: [];
|
||||
const options = source.map((option) => {
|
||||
if (option && typeof option === "object") {
|
||||
const value = option.value ?? option.key ?? option.id;
|
||||
return { ...option, value, label: String(option.label ?? option.name ?? value ?? "") };
|
||||
}
|
||||
return { value: option, label: String(option ?? "") };
|
||||
});
|
||||
return { optionConfig, options };
|
||||
} catch {
|
||||
return { optionConfig: {}, options: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export function isRangeQuery(item) {
|
||||
return (
|
||||
item.config.b_operator === "between" ||
|
||||
item.config.b_component === "number-range" ||
|
||||
item.field.b_type === "datetime"
|
||||
);
|
||||
}
|
||||
|
||||
export function isNumericField(item) {
|
||||
return item.field.b_type === "number" || item.field.b_type === "money";
|
||||
}
|
||||
|
||||
export function isDateQuery(item) {
|
||||
return (
|
||||
item.config.b_component === "date-picker" ||
|
||||
item.field.b_type === "date" ||
|
||||
item.field.b_type === "datetime"
|
||||
);
|
||||
}
|
||||
|
||||
export function emptyQueryValue(item) {
|
||||
if (isRangeQuery(item) || item.config.b_operator === "in") return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function defaultQueryValue(item) {
|
||||
const value = item.config.b_default_value;
|
||||
if (value === null || value === undefined || value === "") return emptyQueryValue(item);
|
||||
if (isRangeQuery(item) || item.config.b_operator === "in") {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
} catch {
|
||||
// 非 JSON 默认值继续按逗号分隔处理。
|
||||
}
|
||||
return String(value)
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (isNumericField(item)) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function formatDateValue(value, withTime = false) {
|
||||
if (isEmptyValue(value)) return "";
|
||||
if (value && typeof value.format === "function") {
|
||||
return value.format(withTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD");
|
||||
}
|
||||
const text = String(value).replace("T", " ");
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(text)) return withTime ? text.slice(0, 19) : text.slice(0, 10);
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return text;
|
||||
const pad = (number) => String(number).padStart(2, "0");
|
||||
const datePart = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
return withTime
|
||||
? `${datePart} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
: datePart;
|
||||
}
|
||||
@@ -1,102 +1,16 @@
|
||||
import { computed, onBeforeUnmount, reactive, ref, shallowRef, unref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { loadDataApi, pageDataApi } from "@/services/api";
|
||||
|
||||
const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
|
||||
function escapeSqlText(value) {
|
||||
return String(value).replaceAll("'", "''");
|
||||
}
|
||||
|
||||
function requireModuleId(value) {
|
||||
const id = String(value ?? "");
|
||||
if (!/^\d+$/.test(id)) throw new Error("模块 ID 格式不正确");
|
||||
return id;
|
||||
}
|
||||
|
||||
function parseOptions(field) {
|
||||
if (!field?.b_options) return { optionConfig: {}, options: [] };
|
||||
try {
|
||||
const parsed = JSON.parse(field.b_options);
|
||||
const optionConfig = Array.isArray(parsed) ? {} : parsed || {};
|
||||
const source = Array.isArray(parsed)
|
||||
? parsed
|
||||
: Array.isArray(parsed?.options)
|
||||
? parsed.options
|
||||
: Array.isArray(parsed?.items)
|
||||
? parsed.items
|
||||
: [];
|
||||
const options = source.map((option) => {
|
||||
if (option && typeof option === "object") {
|
||||
const value = option.value ?? option.key ?? option.id;
|
||||
return { ...option, value, label: String(option.label ?? option.name ?? value ?? "") };
|
||||
}
|
||||
return { value: option, label: String(option ?? "") };
|
||||
});
|
||||
return { optionConfig, options };
|
||||
} catch {
|
||||
return { optionConfig: {}, options: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function isRangeQuery(item) {
|
||||
return (
|
||||
item.config.b_operator === "between" ||
|
||||
item.config.b_component === "number-range" ||
|
||||
item.field.b_type === "datetime"
|
||||
);
|
||||
}
|
||||
|
||||
function isNumericField(item) {
|
||||
return item.field.b_type === "number" || item.field.b_type === "money";
|
||||
}
|
||||
|
||||
function emptyQueryValue(item) {
|
||||
if (isRangeQuery(item) || item.config.b_operator === "in") return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function defaultQueryValue(item) {
|
||||
const value = item.config.b_default_value;
|
||||
if (value === null || value === undefined || value === "") return emptyQueryValue(item);
|
||||
if (isRangeQuery(item) || item.config.b_operator === "in") {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
} catch {
|
||||
// 非 JSON 默认值继续按逗号分隔处理。
|
||||
}
|
||||
return String(value)
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (isNumericField(item)) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isEmptyValue(value) {
|
||||
return value === undefined || value === null || value === "";
|
||||
}
|
||||
|
||||
function formatDateValue(value, withTime = false) {
|
||||
if (isEmptyValue(value)) return "";
|
||||
if (value && typeof value.format === "function") {
|
||||
return value.format(withTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD");
|
||||
}
|
||||
const text = String(value).replace("T", " ");
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(text)) return withTime ? text.slice(0, 19) : text.slice(0, 10);
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return text;
|
||||
const pad = (number) => String(number).padStart(2, "0");
|
||||
const datePart = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
return withTime
|
||||
? `${datePart} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
: datePart;
|
||||
}
|
||||
import {
|
||||
IDENTIFIER_RE,
|
||||
defaultQueryValue,
|
||||
escapeSqlText,
|
||||
formatDateValue,
|
||||
isEmptyValue,
|
||||
isRangeQuery,
|
||||
parseOptions,
|
||||
requireModuleId,
|
||||
} from "./moduleListUtils";
|
||||
|
||||
function formatSqlDate(value, fieldType, endOfRange = false) {
|
||||
if (value && typeof value.format === "function") {
|
||||
|
||||
@@ -427,7 +427,7 @@ defineExpose({
|
||||
:data-source="dataSource"
|
||||
:drag-row-config="{ mode: rowDraggable ? 'insert' : 'none' }"
|
||||
:row-key="rowKey"
|
||||
:bordered="bordered"
|
||||
:bordered="props.editable ? true : 'body-h'"
|
||||
:no-data-full="noDataFull"
|
||||
:show-overflow="showOverflow"
|
||||
:area-selection="editable"
|
||||
|
||||
Reference in new issue
Block a user