20260728225443
This commit is contained in:
1 parent
5ec7aadef9
commit
f6b79ea09a
30 files changed
+1541
-1120
No files matched your search
@@ -7,7 +7,6 @@ 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 },
|
||||
@@ -139,7 +138,6 @@ onBeforeUnmount(() => document.removeEventListener("pointerdown", onDocumentPoin
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:allow-clear="allowClear"
|
||||
:maxlength="maxlength"
|
||||
@update:value="updateValue"
|
||||
@change="onInputChange"
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
<script setup>
|
||||
import { computed, h, nextTick, ref, toRef, useSlots, watch } from "vue";
|
||||
import { computed, h, nextTick, reactive, ref, shallowRef, useSlots, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { message } from "antdv-next";
|
||||
import { Columns3, RefreshCw } from "@lucide/vue";
|
||||
import { loadDataApi, pageDataApi } from "@/services/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import {
|
||||
identifierRe,
|
||||
buildQueryCondition,
|
||||
defaultQueryValue,
|
||||
escapeSqlText,
|
||||
parseOptions,
|
||||
requireModuleId,
|
||||
} from "./moduleListUtils";
|
||||
import FmsModuleTable from "./FmsModuleTable.vue";
|
||||
import FmsQueryToolbar from "./FmsQueryToolbar.vue";
|
||||
import { useModuleList } from "./useModuleList";
|
||||
|
||||
const props = defineProps({
|
||||
dataCode: { type: String, default: "" },
|
||||
columnExtensions: { type: Object, default: () => ({}) },
|
||||
rowActions: { type: Array, default: () => [] },
|
||||
rowSelection: { type: Object, default: null },
|
||||
rowClassName: { type: Function, default: undefined },
|
||||
expandable: { type: [Boolean, Object], default: false },
|
||||
pageSize: { type: Number, default: 20 },
|
||||
@@ -18,25 +29,304 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(["row-click", "cell-click", "row-action", "expand", "loaded"]);
|
||||
const slots = useSlots();
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const tableRef = ref(null);
|
||||
const moduleList = useModuleList({
|
||||
dataCode: toRef(props, "dataCode"),
|
||||
pageSize: props.pageSize,
|
||||
|
||||
// ── 内部状态 ──
|
||||
const dataModule = ref(null);
|
||||
const activeFields = shallowRef([]);
|
||||
const activeFieldById = shallowRef(new Map());
|
||||
const listConfig = shallowRef([]);
|
||||
const queryItems = shallowRef([]);
|
||||
const dataSource = shallowRef([]);
|
||||
const searchConditions = shallowRef([]);
|
||||
const keyField = ref("");
|
||||
const configLoading = ref(false);
|
||||
const dataLoading = ref(false);
|
||||
const configError = ref("");
|
||||
const dataError = ref("");
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: Number(props.pageSize) || 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const moreMenuItems = [
|
||||
{ key: "refresh", label: "刷新数据", icon: h(RefreshCw, { size: 14 }) },
|
||||
{ key: "reset-columns", label: "恢复配置列宽", icon: h(Columns3, { size: 14 }) },
|
||||
];
|
||||
const loading = computed(() => configLoading.value || dataLoading.value);
|
||||
|
||||
// ── 外部工具 ──
|
||||
const moreMenuItems = computed(() => [
|
||||
{
|
||||
key: "refresh",
|
||||
label: appStore.t("common.refreshData", "刷新数据"),
|
||||
icon: h(RefreshCw, { size: 14 }),
|
||||
},
|
||||
{
|
||||
key: "reset-columns",
|
||||
label: appStore.t("common.resetColumns", "恢复配置列宽"),
|
||||
icon: h(Columns3, { size: 14 }),
|
||||
},
|
||||
]);
|
||||
const cellSlotNames = computed(() => Object.keys(slots).filter((name) => name.startsWith("cell-")));
|
||||
|
||||
// ── 模块配置加载 ──
|
||||
function resolveKeyField(module, fields) {
|
||||
if (identifierRe.test(module.b_keyfield || "")) return module.b_keyfield;
|
||||
for (const field of fields) {
|
||||
if (field.b_field === "b_id") return field.b_field;
|
||||
}
|
||||
for (const field of fields) {
|
||||
if (identifierRe.test(field.b_field || "")) return field.b_field;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getDataCode() {
|
||||
return String(props.dataCode || route.meta.dataCode || "").trim();
|
||||
}
|
||||
|
||||
async function loadModuleConfig() {
|
||||
const code = getDataCode();
|
||||
const currentPath = route.path;
|
||||
configLoading.value = true;
|
||||
configError.value = "";
|
||||
|
||||
try {
|
||||
let runtimeData = null;
|
||||
|
||||
if (code) {
|
||||
const moduleRes = await loadDataApi(
|
||||
"s_module",
|
||||
`b_code = N'${escapeSqlText(code)}' AND b_module_type = 'data' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
runtimeData = moduleRes.data?.[0];
|
||||
if (!runtimeData) throw new Error(`未找到编码为 ${code} 的启用数据模块`);
|
||||
|
||||
const pageId = requireModuleId(runtimeData.b_parent_id);
|
||||
const pageRes = await loadDataApi(
|
||||
"s_module",
|
||||
`b_id = ${pageId} AND b_module_type = 'page' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
if (!pageRes.data?.[0]) {
|
||||
throw new Error(`数据编码 ${runtimeData.b_code} 没有对应的启用页面`);
|
||||
}
|
||||
} else {
|
||||
const routePath = escapeSqlText(currentPath);
|
||||
const pageRes = await loadDataApi(
|
||||
"s_module",
|
||||
`b_route = N'${routePath}' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
const page = pageRes.data?.[0];
|
||||
if (!page) throw new Error(`未找到路由 ${currentPath} 对应的启用模块`);
|
||||
|
||||
if (page.b_viewtable) {
|
||||
runtimeData = page;
|
||||
} else {
|
||||
const pageId = requireModuleId(page.b_id);
|
||||
const dataRes = await loadDataApi(
|
||||
"s_module",
|
||||
`b_parent_id = ${pageId} AND b_module_type = 'data' AND b_canuse = 1`,
|
||||
"b_xh ASC, b_id ASC",
|
||||
);
|
||||
for (const item of dataRes.data || []) {
|
||||
if (item.b_viewtable) {
|
||||
runtimeData = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!runtimeData) throw new Error("当前页面没有配置可用的数据模块");
|
||||
}
|
||||
}
|
||||
|
||||
const dataId = requireModuleId(runtimeData.b_id);
|
||||
const responses = await Promise.all([
|
||||
loadDataApi("s_module_field", `b_module_id = ${dataId}`, "b_id ASC"),
|
||||
loadDataApi("s_module_field_list", `b_module_id = ${dataId}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi(
|
||||
"s_module_field_query",
|
||||
`b_module_id = ${dataId} AND b_canuse = 1`,
|
||||
"b_xh ASC, b_id ASC",
|
||||
),
|
||||
]);
|
||||
if (getDataCode() !== code || route.path !== currentPath) return false;
|
||||
|
||||
const fields = [];
|
||||
const fieldById = new Map();
|
||||
for (const field of responses[0].data || []) {
|
||||
if (field.b_canuse !== 1 || !identifierRe.test(field.b_field || "")) continue;
|
||||
fields.push(field);
|
||||
fieldById.set(String(field.b_id), field);
|
||||
}
|
||||
|
||||
const lists = [];
|
||||
for (const item of responses[1].data || []) {
|
||||
if (item.b_visible !== 1) continue;
|
||||
if (!fieldById.has(String(item.b_field_id))) continue;
|
||||
lists.push(item);
|
||||
}
|
||||
|
||||
const queries = [];
|
||||
for (const config of responses[2].data || []) {
|
||||
const field = fieldById.get(String(config.b_field_id));
|
||||
if (!field) continue;
|
||||
const optionInfo = parseOptions(field);
|
||||
queries.push({
|
||||
config,
|
||||
field,
|
||||
optionConfig: optionInfo.optionConfig,
|
||||
options: optionInfo.options,
|
||||
});
|
||||
}
|
||||
|
||||
const conditions = [];
|
||||
for (const item of queries) {
|
||||
const condition = buildQueryCondition(item, defaultQueryValue(item));
|
||||
if (condition) conditions.push(condition);
|
||||
}
|
||||
|
||||
dataModule.value = runtimeData;
|
||||
activeFields.value = fields;
|
||||
activeFieldById.value = fieldById;
|
||||
listConfig.value = lists;
|
||||
queryItems.value = queries;
|
||||
searchConditions.value = conditions;
|
||||
keyField.value = resolveKeyField(runtimeData, fields);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (getDataCode() === code && route.path === currentPath) {
|
||||
configError.value = error?.message || String(error);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (getDataCode() === code && route.path === currentPath) {
|
||||
configLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 数据查询 ──
|
||||
function resolveOrderBy() {
|
||||
if (!dataModule.value) throw new Error("数据模块尚未加载");
|
||||
const configured = String(dataModule.value.b_orderfield || "").trim();
|
||||
if (configured) return configured;
|
||||
if (keyField.value) return `${keyField.value} DESC`;
|
||||
|
||||
if (listConfig.value.length > 0) {
|
||||
const firstConfig = listConfig.value[0];
|
||||
const firstField = activeFieldById.value.get(String(firstConfig.b_field_id));
|
||||
if (firstField && identifierRe.test(firstField.b_field || "")) {
|
||||
return `${firstField.b_field} ASC`;
|
||||
}
|
||||
}
|
||||
throw new Error("数据模块未配置可用的默认排序字段");
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
if (!dataModule.value || !dataModule.value.b_viewtable || dataLoading.value) return;
|
||||
|
||||
const currentModule = dataModule.value;
|
||||
const viewName = currentModule.b_viewtable;
|
||||
const currentCode = getDataCode();
|
||||
const currentPath = route.path;
|
||||
dataLoading.value = true;
|
||||
dataError.value = "";
|
||||
|
||||
try {
|
||||
const res = await pageDataApi({
|
||||
viewName,
|
||||
orderBy: resolveOrderBy(),
|
||||
searchColumns: [],
|
||||
searchCondition: searchConditions.value.join(" AND "),
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
|
||||
if (dataModule.value !== currentModule) return;
|
||||
if (getDataCode() !== currentCode || route.path !== currentPath) return;
|
||||
dataSource.value = res.data?.rows || [];
|
||||
pagination.total = Number(res.data?.total ?? res.data?.row_count) || 0;
|
||||
} catch (error) {
|
||||
if (
|
||||
dataModule.value === currentModule &&
|
||||
getDataCode() === currentCode &&
|
||||
route.path === currentPath
|
||||
) {
|
||||
dataSource.value = [];
|
||||
pagination.total = 0;
|
||||
dataError.value = error?.message || String(error);
|
||||
}
|
||||
} finally {
|
||||
if (
|
||||
dataModule.value === currentModule &&
|
||||
getDataCode() === currentCode &&
|
||||
route.path === currentPath
|
||||
) {
|
||||
dataLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 公开方法 ──
|
||||
async function initialize() {
|
||||
dataModule.value = null;
|
||||
activeFields.value = [];
|
||||
activeFieldById.value = new Map();
|
||||
listConfig.value = [];
|
||||
queryItems.value = [];
|
||||
searchConditions.value = [];
|
||||
keyField.value = "";
|
||||
dataLoading.value = false;
|
||||
dataError.value = "";
|
||||
pagination.current = 1;
|
||||
dataSource.value = [];
|
||||
pagination.total = 0;
|
||||
|
||||
const loaded = await loadModuleConfig();
|
||||
if (loaded) await fetchData();
|
||||
}
|
||||
|
||||
function search({ entries = [] }) {
|
||||
if (loading.value) return;
|
||||
const conditions = [];
|
||||
for (const entry of entries) {
|
||||
const condition = buildQueryCondition(entry.item, entry.value);
|
||||
if (condition) conditions.push(condition);
|
||||
}
|
||||
searchConditions.value = conditions;
|
||||
pagination.current = 1;
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
if (loading.value) return;
|
||||
searchConditions.value = [];
|
||||
pagination.current = 1;
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
function changePage(page, pageSize) {
|
||||
if (loading.value) return;
|
||||
pagination.current = pageSize === pagination.pageSize ? page : 1;
|
||||
pagination.pageSize = pageSize;
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
function reload() {
|
||||
return moduleList.reload();
|
||||
if (loading.value) return;
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
async function resetColumns() {
|
||||
await nextTick();
|
||||
tableRef.value?.resetColumns?.();
|
||||
if (tableRef.value) tableRef.value.resetColumns();
|
||||
}
|
||||
|
||||
function resolveRowKey(row) {
|
||||
if (keyField.value) return row[keyField.value];
|
||||
return JSON.stringify(row);
|
||||
}
|
||||
|
||||
async function handleMoreAction({ key }) {
|
||||
@@ -46,57 +336,57 @@ async function handleMoreAction({ key }) {
|
||||
}
|
||||
if (key === "reset-columns") {
|
||||
await resetColumns();
|
||||
message.success("已恢复配置列宽");
|
||||
message.success(appStore.t("common.columnsReset", "已恢复配置列宽"));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 生命周期 ──
|
||||
watch(() => [getDataCode(), route.path], initialize, { immediate: true });
|
||||
|
||||
watch(
|
||||
() => moduleList.dataSource.value,
|
||||
() => dataSource.value,
|
||||
(rows) => {
|
||||
emit("loaded", {
|
||||
rows,
|
||||
total: moduleList.pagination.total,
|
||||
module: moduleList.dataModule.value,
|
||||
});
|
||||
emit("loaded", { rows, total: pagination.total, module: dataModule.value });
|
||||
},
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
reload,
|
||||
resetColumns,
|
||||
initialize: moduleList.initialize,
|
||||
dataSource: moduleList.dataSource,
|
||||
dataModule: moduleList.dataModule,
|
||||
pagination: moduleList.pagination,
|
||||
initialize,
|
||||
dataSource,
|
||||
dataModule,
|
||||
pagination,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fms-module-list-page">
|
||||
<div v-if="moduleList.configError.value" class="module-config-error">
|
||||
<a-result status="error" title="模块配置加载失败" :sub-title="moduleList.configError.value">
|
||||
<div v-if="configError" class="module-config-error">
|
||||
<a-result
|
||||
status="error"
|
||||
:title="appStore.t('common.moduleConfigLoadFailed', '模块配置加载失败')"
|
||||
:sub-title="configError"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="moduleList.initialize">重新加载</a-button>
|
||||
<a-button type="primary" @click="initialize">
|
||||
{{ appStore.t("common.reload", "重新加载") }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<FmsQueryToolbar
|
||||
:query-items="moduleList.queryItems.value"
|
||||
:loading="moduleList.dataLoading.value"
|
||||
:query-items="queryItems"
|
||||
:loading="loading"
|
||||
:more-items="moreMenuItems"
|
||||
@search="moduleList.search"
|
||||
@reset="moduleList.resetSearch"
|
||||
@search="search"
|
||||
@reset="resetSearch"
|
||||
@more-action="handleMoreAction"
|
||||
>
|
||||
<template #actions>
|
||||
<slot
|
||||
name="actions"
|
||||
:reload="reload"
|
||||
:data-module="moduleList.dataModule.value"
|
||||
:loading="moduleList.loading.value"
|
||||
/>
|
||||
<slot name="actions" :reload="reload" :data-module="dataModule" :loading="loading" />
|
||||
</template>
|
||||
</FmsQueryToolbar>
|
||||
|
||||
@@ -104,12 +394,13 @@ defineExpose({
|
||||
<div class="module-table-stage">
|
||||
<FmsModuleTable
|
||||
ref="tableRef"
|
||||
:fields="moduleList.activeFields.value"
|
||||
:list-config="moduleList.listConfig.value"
|
||||
:data-source="moduleList.dataSource.value"
|
||||
:row-key="moduleList.resolveRowKey"
|
||||
:fields="activeFields"
|
||||
:list-config="listConfig"
|
||||
:data-source="dataSource"
|
||||
:row-key="resolveRowKey"
|
||||
:column-extensions="columnExtensions"
|
||||
:row-actions="rowActions"
|
||||
:row-selection="rowSelection"
|
||||
:row-class-name="rowClassName"
|
||||
:expandable="expandable"
|
||||
:inline-action-limit="inlineActionLimit"
|
||||
@@ -129,22 +420,26 @@ defineExpose({
|
||||
</template>
|
||||
</FmsModuleTable>
|
||||
|
||||
<div v-if="moduleList.loading.value" class="module-table-loading">
|
||||
<div v-if="loading" class="module-table-loading">
|
||||
<a-spin />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="moduleList.dataError.value" class="module-data-error">
|
||||
{{ moduleList.dataError.value }}
|
||||
<div v-if="dataError" class="module-data-error">
|
||||
{{ dataError }}
|
||||
</div>
|
||||
|
||||
<a-pagination
|
||||
:current="moduleList.pagination.current"
|
||||
:page-size="moduleList.pagination.pageSize"
|
||||
:total="moduleList.pagination.total"
|
||||
:show-total="(total) => `共 ${total} 条`"
|
||||
:current="pagination.current"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:show-total="
|
||||
(total) =>
|
||||
`${appStore.t('common.total', '共')} ${total} ${appStore.t('common.items', '条')}`
|
||||
"
|
||||
:disabled="loading"
|
||||
show-size-changer
|
||||
@change="moduleList.changePage"
|
||||
@change="changePage"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, h, ref, shallowRef, useSlots, watch } from "vue";
|
||||
import { Checkbox } from "antdv-next";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { formatDateValue, parseOptions } from "./moduleListUtils";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -10,12 +12,14 @@ const props = defineProps({
|
||||
rowKey: { type: [String, Function], default: "b_id" },
|
||||
columnExtensions: { type: Object, default: () => ({}) },
|
||||
rowActions: { type: Array, default: () => [] },
|
||||
rowSelection: { type: Object, default: null },
|
||||
rowClassName: { type: Function, default: undefined },
|
||||
expandable: { type: [Boolean, Object], default: false },
|
||||
inlineActionLimit: { type: Number, default: 2 },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["row-click", "cell-click", "row-action", "expand"]);
|
||||
const appStore = useAppStore();
|
||||
const slots = useSlots();
|
||||
const tableRef = ref(null);
|
||||
const columns = shallowRef([]);
|
||||
@@ -36,10 +40,8 @@ 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.optionConfig.trueLabel ?? "是")
|
||||
: (optionInfo.optionConfig.falseLabel ?? "否");
|
||||
if (field.b_type === "checkbox") {
|
||||
return h(Checkbox, { checked: value === true || Number(value) === 1, disabled: true });
|
||||
}
|
||||
if (field.b_type === "select") {
|
||||
const option = optionInfo.options.find((item) => String(item.value) === String(value));
|
||||
@@ -69,10 +71,12 @@ function createRuntimeColumn(config, field) {
|
||||
const { formatter, clickable, cellClassName, onClick: _onClick, ...columnProps } = extension;
|
||||
const slot = slots[`cell-${field.b_field}`];
|
||||
const column = {
|
||||
title: field.b_name || field.b_field,
|
||||
title: appStore.t(field.b_i18n, field.b_name || field.b_field),
|
||||
dataIndex: field.b_field,
|
||||
key: field.b_field,
|
||||
width: Number(config.b_width) || 120,
|
||||
checkbox: field.b_type === "checkbox",
|
||||
align: field.b_type === "checkbox" ? "center" : undefined,
|
||||
...columnProps,
|
||||
};
|
||||
|
||||
@@ -141,12 +145,16 @@ function createActionColumn() {
|
||||
const inlineActions = actions.slice(0, inlineLimit);
|
||||
const overflowActions = actions.slice(inlineLimit);
|
||||
if (overflowActions.length) {
|
||||
inlineActions.push({ type: "more", title: "更多操作", items: overflowActions });
|
||||
inlineActions.push({
|
||||
type: "more",
|
||||
title: appStore.t("common.moreActions", "更多操作"),
|
||||
items: overflowActions,
|
||||
});
|
||||
}
|
||||
return {
|
||||
dataIndex: "__actions__",
|
||||
key: "__actions__",
|
||||
title: "操作",
|
||||
title: appStore.t("common.operation", "操作"),
|
||||
width: Math.max(70, inlineActions.length * 52),
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
@@ -215,11 +223,11 @@ function handleToggleExpand(payload) {
|
||||
if (payload.expanded && single && expandedRecord && expandedRecord !== payload.row) {
|
||||
tableRef.value?.setRowExpand?.(expandedRecord, false, { silent: true });
|
||||
}
|
||||
expandedRecord = payload.expanded
|
||||
? payload.row
|
||||
: expandedRecord === payload.row
|
||||
? null
|
||||
: expandedRecord;
|
||||
if (payload.expanded) {
|
||||
expandedRecord = payload.row;
|
||||
} else if (expandedRecord === payload.row) {
|
||||
expandedRecord = null;
|
||||
}
|
||||
emit("expand", { expanded: payload.expanded, record: payload.row, column: payload.col });
|
||||
}
|
||||
|
||||
@@ -229,6 +237,7 @@ watch(
|
||||
() => props.listConfig,
|
||||
() => props.columnExtensions,
|
||||
() => props.rowActions,
|
||||
() => appStore.locale,
|
||||
expansionEnabled,
|
||||
],
|
||||
rebuildColumns,
|
||||
@@ -251,6 +260,7 @@ defineExpose({ resetColumns, clearSelectedArea: () => tableRef.value?.clearSelec
|
||||
v-model:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:row-key="rowKey"
|
||||
:row-selection="rowSelection"
|
||||
:row-class-name="rowClassName"
|
||||
:expand-config="expansionEnabled ? expandConfig : undefined"
|
||||
:editable="false"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { isDateQuery, isEmptyValue, isNumericField, isRangeQuery } from "./moduleListUtils";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -9,10 +10,17 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "enter"]);
|
||||
const appStore = useAppStore();
|
||||
|
||||
const isRange = computed(() => isRangeQuery(props.item));
|
||||
const isNumeric = computed(() => isNumericField(props.item));
|
||||
const isDate = computed(() => isDateQuery(props.item));
|
||||
const isCheckbox = computed(() => props.item.field.b_type === "checkbox");
|
||||
const checkboxOptions = computed(() => [
|
||||
{ label: appStore.t("common.all", "全部"), value: "" },
|
||||
{ label: appStore.t("common.yes", "是"), value: "1" },
|
||||
{ label: appStore.t("common.no", "否"), value: "0" },
|
||||
]);
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (nextValue) => emit("update:modelValue", nextValue),
|
||||
@@ -92,6 +100,13 @@ function isDateBeforeLimit(current) {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<a-radio-group
|
||||
v-else-if="isCheckbox"
|
||||
v-model:value="value"
|
||||
class="fms-query-control__boolean"
|
||||
:options="checkboxOptions"
|
||||
option-type="button"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="item.config.b_component === 'select'"
|
||||
v-model:value="value"
|
||||
@@ -131,6 +146,18 @@ function isDateBeforeLimit(current) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fms-query-control__boolean {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
:deep(.ant-radio-button-wrapper) {
|
||||
min-width: 42px;
|
||||
flex: 1;
|
||||
padding-inline: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.fms-query-control__range--compact {
|
||||
grid-template-columns: minmax(0, 1fr) 24px minmax(0, 1fr);
|
||||
gap: 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ChevronDown, ChevronUp, Ellipsis, RotateCcw, Search } from "@lucide/vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import FmsQueryControl from "./FmsQueryControl.vue";
|
||||
import {
|
||||
defaultQueryValue,
|
||||
@@ -18,6 +19,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(["search", "reset", "more-action"]);
|
||||
const appStore = useAppStore();
|
||||
|
||||
const filters = reactive({});
|
||||
const quickFieldName = ref("");
|
||||
@@ -29,7 +31,7 @@ const quickFieldOptions = computed(() =>
|
||||
props.queryItems.map((item) => ({
|
||||
key: item.field.b_field,
|
||||
value: item.field.b_field,
|
||||
label: item.field.b_name || item.field.b_field,
|
||||
label: appStore.t(item.field.b_i18n, item.field.b_name || item.field.b_field),
|
||||
})),
|
||||
);
|
||||
const quickItem = computed(() =>
|
||||
@@ -133,19 +135,30 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
|
||||
type="primary"
|
||||
class="quick-submit"
|
||||
:loading="loading"
|
||||
aria-label="搜索"
|
||||
:aria-label="appStore.t('common.search', '搜索')"
|
||||
@click="handleQuickSearch"
|
||||
>
|
||||
<Search :size="16" />
|
||||
</a-button>
|
||||
</a-space-compact>
|
||||
|
||||
<a-tooltip v-if="queryItems.length" :title="advancedOpen ? '收起高级搜索' : '高级搜索'">
|
||||
<a-tooltip
|
||||
v-if="queryItems.length"
|
||||
:title="
|
||||
advancedOpen
|
||||
? appStore.t('common.collapseAdvancedSearch', '收起高级搜索')
|
||||
: appStore.t('common.advancedSearch', '高级搜索')
|
||||
"
|
||||
>
|
||||
<a-button
|
||||
shape="circle"
|
||||
class="toolbar-icon-button"
|
||||
:class="{ 'toolbar-icon-button--active': advancedOpen }"
|
||||
:aria-label="advancedOpen ? '收起高级搜索' : '高级搜索'"
|
||||
:aria-label="
|
||||
advancedOpen
|
||||
? appStore.t('common.collapseAdvancedSearch', '收起高级搜索')
|
||||
: appStore.t('common.advancedSearch', '高级搜索')
|
||||
"
|
||||
@click="toggleAdvancedSearch"
|
||||
>
|
||||
<Search :size="16" />
|
||||
@@ -154,13 +167,18 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
|
||||
|
||||
<a-dropdown
|
||||
v-if="moreItems.length"
|
||||
:disabled="loading"
|
||||
:menu="{ items: moreItems, onClick: (event) => $emit('more-action', event) }"
|
||||
:trigger="['click']"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<span class="more-trigger">
|
||||
<a-tooltip title="更多">
|
||||
<a-button shape="circle" class="toolbar-icon-button" aria-label="更多">
|
||||
<a-tooltip :title="appStore.t('common.more', '更多')">
|
||||
<a-button
|
||||
shape="circle"
|
||||
class="toolbar-icon-button"
|
||||
:aria-label="appStore.t('common.more', '更多')"
|
||||
>
|
||||
<Ellipsis :size="17" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
@@ -176,7 +194,7 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
|
||||
v-for="item in visibleAdvancedQueryItems"
|
||||
:key="item.field.b_field"
|
||||
:class="{ 'query-field--wide': isRangeQuery(item) }"
|
||||
:label="item.field.b_name"
|
||||
:label="appStore.t(item.field.b_i18n, item.field.b_name)"
|
||||
>
|
||||
<FmsQueryControl
|
||||
v-model="filters[item.field.b_field]"
|
||||
@@ -195,18 +213,22 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
|
||||
>
|
||||
<ChevronUp v-if="advancedMoreOpen" :size="15" />
|
||||
<ChevronDown v-else :size="15" />
|
||||
{{ advancedMoreOpen ? "收起条件" : `更多条件 (${hiddenAdvancedQueryCount})` }}
|
||||
{{
|
||||
advancedMoreOpen
|
||||
? appStore.t("common.collapseConditions", "收起条件")
|
||||
: `${appStore.t("common.moreConditions", "更多条件")} (${hiddenAdvancedQueryCount})`
|
||||
}}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="query-actions">
|
||||
<a-button type="primary" :loading="loading" @click="handleAdvancedSearch">
|
||||
<Search :size="15" />
|
||||
查询
|
||||
{{ appStore.t("common.search", "查询") }}
|
||||
</a-button>
|
||||
<a-button :disabled="loading" @click="handleAdvancedReset">
|
||||
<RotateCcw :size="15" />
|
||||
重置
|
||||
{{ appStore.t("common.reset", "重置") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
@@ -295,9 +317,9 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
|
||||
|
||||
.query-panel {
|
||||
display: grid;
|
||||
width: min(1100px, calc(100% - 24px));
|
||||
width: min(950px, calc(100% - 24px));
|
||||
align-self: flex-start;
|
||||
grid-template-columns: minmax(0, 1fr) 160px;
|
||||
grid-template-columns: minmax(0, 1fr) 172px;
|
||||
gap: 14px;
|
||||
padding: 10px 12px 12px;
|
||||
border-left: 1px solid var(--fms-border, #e5e7eb);
|
||||
@@ -350,6 +372,7 @@ watch(() => props.queryItems, initializeDrafts, { immediate: true });
|
||||
.query-actions {
|
||||
display: flex;
|
||||
width: 160px;
|
||||
margin-left: 12px;
|
||||
align-self: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 数据驱动列表的共享工具函数,避免在各组件间重复实现查询/格式化逻辑。
|
||||
|
||||
export const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
export const identifierRe = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
|
||||
export function escapeSqlText(value) {
|
||||
return String(value).replaceAll("'", "''");
|
||||
@@ -20,14 +20,18 @@ 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
|
||||
: [];
|
||||
let optionConfig = {};
|
||||
let source = [];
|
||||
if (Array.isArray(parsed)) {
|
||||
source = parsed;
|
||||
} else if (parsed && typeof parsed === "object") {
|
||||
optionConfig = parsed;
|
||||
if (Array.isArray(parsed.options)) {
|
||||
source = parsed.options;
|
||||
} else if (Array.isArray(parsed.items)) {
|
||||
source = parsed.items;
|
||||
}
|
||||
}
|
||||
const options = source.map((option) => {
|
||||
if (option && typeof option === "object") {
|
||||
const value = option.value ?? option.key ?? option.id;
|
||||
@@ -62,6 +66,7 @@ export function isDateQuery(item) {
|
||||
}
|
||||
|
||||
export function emptyQueryValue(item) {
|
||||
if (item.field.b_type === "checkbox") return "";
|
||||
if (isRangeQuery(item) || item.config.b_operator === "in") return [];
|
||||
return undefined;
|
||||
}
|
||||
@@ -88,6 +93,72 @@ export function defaultQueryValue(item) {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function sqlLiteral(value, field, endOfRange = false) {
|
||||
if (isEmptyValue(value)) return null;
|
||||
|
||||
if (field.b_type === "number" || field.b_type === "money") {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? String(number) : null;
|
||||
}
|
||||
|
||||
if (field.b_type === "checkbox") {
|
||||
return value === true || Number(value) === 1 ? "1" : "0";
|
||||
}
|
||||
|
||||
let normalized = String(value);
|
||||
const isDate = field.b_type === "date" || field.b_type === "datetime";
|
||||
if (isDate && value && typeof value.format === "function") {
|
||||
normalized = value.format("YYYY-MM-DD");
|
||||
} else if (isDate) {
|
||||
normalized = String(value).slice(0, 10);
|
||||
}
|
||||
|
||||
if (field.b_type === "datetime") {
|
||||
normalized += endOfRange ? " 23:59:59" : " 00:00:00";
|
||||
}
|
||||
|
||||
return `N'${escapeSqlText(normalized)}'`;
|
||||
}
|
||||
|
||||
export function buildQueryCondition(item, value) {
|
||||
const fieldName = item.field.b_field;
|
||||
if (!identifierRe.test(fieldName)) return "";
|
||||
|
||||
const field = `[${fieldName}]`;
|
||||
if (isRangeQuery(item)) {
|
||||
const values = Array.isArray(value) ? value : [];
|
||||
const start = sqlLiteral(values[0], item.field);
|
||||
const end = sqlLiteral(values[1], item.field, true);
|
||||
if (start && end) return `${field} BETWEEN ${start} AND ${end}`;
|
||||
if (start) return `${field} >= ${start}`;
|
||||
if (end) return `${field} <= ${end}`;
|
||||
return "";
|
||||
}
|
||||
|
||||
if (item.config.b_operator === "in") {
|
||||
let values = value;
|
||||
if (!Array.isArray(values)) {
|
||||
values = String(value ?? "")
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
const literals = [];
|
||||
for (const entry of values) {
|
||||
const literal = sqlLiteral(entry, item.field);
|
||||
if (literal) literals.push(literal);
|
||||
}
|
||||
return literals.length ? `${field} IN (${literals.join(", ")})` : "";
|
||||
}
|
||||
|
||||
const literal = sqlLiteral(value, item.field);
|
||||
if (!literal) return "";
|
||||
if (item.config.b_operator === "like") {
|
||||
return `${field} LIKE N'%${escapeSqlText(String(value))}%'`;
|
||||
}
|
||||
return `${field} = ${literal}`;
|
||||
}
|
||||
|
||||
export function formatDateValue(value, withTime = false) {
|
||||
if (isEmptyValue(value)) return "";
|
||||
if (value && typeof value.format === "function") {
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
import { computed, onBeforeUnmount, reactive, ref, shallowRef, unref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { loadDataApi, pageDataApi } from "@/services/api";
|
||||
import {
|
||||
IDENTIFIER_RE,
|
||||
defaultQueryValue,
|
||||
escapeSqlText,
|
||||
formatDateValue,
|
||||
isEmptyValue,
|
||||
isRangeQuery,
|
||||
parseOptions,
|
||||
requireModuleId,
|
||||
} from "./moduleListUtils";
|
||||
|
||||
function formatSqlDate(value, fieldType, endOfRange = false) {
|
||||
if (value && typeof value.format === "function") {
|
||||
if (fieldType === "datetime") {
|
||||
return `${value.format("YYYY-MM-DD")} ${endOfRange ? "23:59:59" : "00:00:00"}`;
|
||||
}
|
||||
return value.format("YYYY-MM-DD");
|
||||
}
|
||||
const date = String(value).slice(0, 10);
|
||||
if (fieldType === "datetime" && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
return `${date} ${endOfRange ? "23:59:59" : "00:00:00"}`;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
function sqlLiteral(value, field, endOfRange = false) {
|
||||
if (isEmptyValue(value)) return null;
|
||||
if (field.b_type === "number" || field.b_type === "money") {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? String(number) : null;
|
||||
}
|
||||
if (field.b_type === "switch") return value === true || Number(value) === 1 ? "1" : "0";
|
||||
const normalized =
|
||||
field.b_type === "date" || field.b_type === "datetime"
|
||||
? formatSqlDate(value, field.b_type, endOfRange)
|
||||
: String(value);
|
||||
return `N'${escapeSqlText(normalized)}'`;
|
||||
}
|
||||
|
||||
function buildQueryCondition(item, value) {
|
||||
const fieldName = item.field.b_field;
|
||||
if (!IDENTIFIER_RE.test(fieldName)) return "";
|
||||
const identifier = `[${fieldName}]`;
|
||||
const operator = item.config.b_operator;
|
||||
|
||||
if (isRangeQuery(item)) {
|
||||
const [start, end] = Array.isArray(value) ? value : [];
|
||||
const startLiteral = sqlLiteral(start, item.field);
|
||||
const endLiteral = sqlLiteral(end, item.field, true);
|
||||
if (startLiteral && endLiteral) {
|
||||
return `${identifier} BETWEEN ${startLiteral} AND ${endLiteral}`;
|
||||
}
|
||||
if (startLiteral) return `${identifier} >= ${startLiteral}`;
|
||||
if (endLiteral) return `${identifier} <= ${endLiteral}`;
|
||||
return "";
|
||||
}
|
||||
|
||||
if (operator === "in") {
|
||||
const values = Array.isArray(value)
|
||||
? value
|
||||
: String(value ?? "")
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
const literals = values.map((entry) => sqlLiteral(entry, item.field)).filter(Boolean);
|
||||
return literals.length ? `${identifier} IN (${literals.join(", ")})` : "";
|
||||
}
|
||||
|
||||
const literal = sqlLiteral(value, item.field);
|
||||
if (!literal) return "";
|
||||
if (operator === "like") return `${identifier} LIKE N'%${escapeSqlText(String(value))}%'`;
|
||||
return `${identifier} = ${literal}`;
|
||||
}
|
||||
|
||||
function displayQueryValue(item, value) {
|
||||
if (isEmptyValue(value)) return "";
|
||||
if (item.field.b_type === "date" || item.field.b_type === "datetime") {
|
||||
return formatDateValue(value);
|
||||
}
|
||||
const option = item.options.find((entry) => String(entry.value) === String(value));
|
||||
return option?.label ?? String(value);
|
||||
}
|
||||
|
||||
function formatAppliedFilterValue(item, value) {
|
||||
if (isRangeQuery(item)) {
|
||||
const [start, end] = Array.isArray(value) ? value : [];
|
||||
const startText = displayQueryValue(item, start);
|
||||
const endText = displayQueryValue(item, end);
|
||||
if (startText && endText) return `${startText} - ${endText}`;
|
||||
if (startText) return `${startText} 起`;
|
||||
if (endText) return `${endText} 止`;
|
||||
return "";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((entry) => displayQueryValue(item, entry))
|
||||
.filter(Boolean)
|
||||
.join("、");
|
||||
}
|
||||
return displayQueryValue(item, value);
|
||||
}
|
||||
|
||||
function createAppliedFilter(item, value) {
|
||||
const condition = buildQueryCondition(item, value);
|
||||
if (!condition) return null;
|
||||
return {
|
||||
key: item.field.b_field,
|
||||
label: item.field.b_name || item.field.b_field,
|
||||
value: formatAppliedFilterValue(item, value),
|
||||
condition,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveKeyField(module, activeFields) {
|
||||
if (IDENTIFIER_RE.test(module?.b_keyfield || "")) return module.b_keyfield;
|
||||
return (
|
||||
activeFields.find((field) => field.b_field === "b_id")?.b_field ||
|
||||
activeFields.find((field) => IDENTIFIER_RE.test(field.b_field || ""))?.b_field ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
export function useModuleList(options = {}) {
|
||||
const route = useRoute();
|
||||
const pageModule = ref(null);
|
||||
const dataModule = ref(null);
|
||||
const activeFields = shallowRef([]);
|
||||
const listConfig = shallowRef([]);
|
||||
const queryItems = shallowRef([]);
|
||||
const dataSource = shallowRef([]);
|
||||
const appliedFilters = shallowRef([]);
|
||||
const configLoading = ref(false);
|
||||
const dataLoading = ref(false);
|
||||
const configError = ref("");
|
||||
const dataError = ref("");
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: Number(unref(options.pageSize)) || 20,
|
||||
total: 0,
|
||||
});
|
||||
let requestVersion = 0;
|
||||
let dataRequestVersion = 0;
|
||||
let disposed = false;
|
||||
|
||||
const loading = computed(() => configLoading.value || dataLoading.value);
|
||||
const appliedSearchCondition = computed(() =>
|
||||
appliedFilters.value.map((item) => item.condition).join(" AND "),
|
||||
);
|
||||
|
||||
function commitAppliedFilters(filters) {
|
||||
appliedFilters.value = filters.filter(Boolean);
|
||||
}
|
||||
|
||||
async function resolvePageModule(runtimeData) {
|
||||
const pageId = requireModuleId(runtimeData.b_parent_id);
|
||||
const response = await loadDataApi(
|
||||
"s_module",
|
||||
`b_id = ${pageId} AND b_module_type = 'page' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
const module = response.data?.[0];
|
||||
if (!module) throw new Error(`数据编码 ${runtimeData.b_code} 没有对应的启用页面`);
|
||||
return module;
|
||||
}
|
||||
|
||||
async function resolveConfiguredModules() {
|
||||
const dataCode = String(unref(options.dataCode) || route.meta.dataCode || "").trim();
|
||||
if (dataCode) {
|
||||
const response = await loadDataApi(
|
||||
"s_module",
|
||||
`b_code = N'${escapeSqlText(dataCode)}' AND b_module_type = 'data' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
const runtimeData = response.data?.[0];
|
||||
if (!runtimeData) throw new Error(`未找到编码为 ${dataCode} 的启用数据模块`);
|
||||
return { runtimePage: await resolvePageModule(runtimeData), runtimeData };
|
||||
}
|
||||
|
||||
const routePath = escapeSqlText(route.path);
|
||||
const response = await loadDataApi(
|
||||
"s_module",
|
||||
`b_route = N'${routePath}' AND b_canuse = 1`,
|
||||
"b_id ASC",
|
||||
);
|
||||
const runtimePage = response.data?.[0];
|
||||
if (!runtimePage) throw new Error(`未找到路由 ${route.path} 对应的启用模块`);
|
||||
return { runtimePage, runtimeData: await resolveDataModule(runtimePage) };
|
||||
}
|
||||
|
||||
async function resolveDataModule(runtimePage) {
|
||||
if (runtimePage.b_viewtable) return runtimePage;
|
||||
const pageId = requireModuleId(runtimePage.b_id);
|
||||
const response = await loadDataApi(
|
||||
"s_module",
|
||||
`b_parent_id = ${pageId} AND b_module_type = 'data' AND b_canuse = 1`,
|
||||
"b_xh ASC, b_id ASC",
|
||||
);
|
||||
const module = response.data?.find((item) => item.b_viewtable);
|
||||
if (!module) throw new Error("当前页面没有配置可用的数据模块");
|
||||
return module;
|
||||
}
|
||||
|
||||
async function loadModuleConfig(version) {
|
||||
configLoading.value = true;
|
||||
configError.value = "";
|
||||
try {
|
||||
const { runtimePage, runtimeData } = await resolveConfiguredModules();
|
||||
const dataId = requireModuleId(runtimeData.b_id);
|
||||
const [fieldResponse, listResponse, queryResponse] = await Promise.all([
|
||||
loadDataApi("s_module_field", `b_module_id = ${dataId}`, "b_id ASC"),
|
||||
loadDataApi("s_module_field_list", `b_module_id = ${dataId}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi(
|
||||
"s_module_field_query",
|
||||
`b_module_id = ${dataId} AND b_canuse = 1`,
|
||||
"b_xh ASC, b_id ASC",
|
||||
),
|
||||
]);
|
||||
if (disposed || version !== requestVersion) return false;
|
||||
|
||||
const fields = (fieldResponse.data || []).filter(
|
||||
(field) => field.b_canuse === 1 && IDENTIFIER_RE.test(field.b_field || ""),
|
||||
);
|
||||
const fieldById = new Map(fields.map((field) => [String(field.b_id), field]));
|
||||
const lists = (listResponse.data || []).filter(
|
||||
(item) => item.b_visible === 1 && fieldById.has(String(item.b_field_id)),
|
||||
);
|
||||
const queries = (queryResponse.data || [])
|
||||
.map((config) => {
|
||||
const field = fieldById.get(String(config.b_field_id));
|
||||
if (!field) return null;
|
||||
return { config, field, ...parseOptions(field) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
runtimeData.resolvedKeyField = resolveKeyField(runtimeData, fields);
|
||||
pageModule.value = runtimePage;
|
||||
dataModule.value = runtimeData;
|
||||
activeFields.value = fields;
|
||||
listConfig.value = lists;
|
||||
queryItems.value = queries;
|
||||
commitAppliedFilters(
|
||||
queries.map((item) => createAppliedFilter(item, defaultQueryValue(item))),
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (disposed || version !== requestVersion) return false;
|
||||
configError.value = error?.message || String(error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (version === requestVersion) configLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOrderBy() {
|
||||
const configured = String(dataModule.value?.b_orderfield || "").trim();
|
||||
if (configured) return configured;
|
||||
const keyField = dataModule.value?.resolvedKeyField;
|
||||
if (keyField) return `${keyField} DESC`;
|
||||
const fieldById = new Map(activeFields.value.map((field) => [String(field.b_id), field]));
|
||||
const firstField = fieldById.get(String(listConfig.value[0]?.b_field_id))?.b_field;
|
||||
if (IDENTIFIER_RE.test(firstField || "")) return `${firstField} ASC`;
|
||||
throw new Error("数据模块未配置可用的默认排序字段");
|
||||
}
|
||||
|
||||
async function fetchData(version = requestVersion) {
|
||||
if (!dataModule.value?.b_viewtable || version !== requestVersion) return;
|
||||
const dataVersion = ++dataRequestVersion;
|
||||
dataLoading.value = true;
|
||||
dataError.value = "";
|
||||
try {
|
||||
const fieldById = new Map(activeFields.value.map((field) => [String(field.b_id), field]));
|
||||
const searchColumns = [
|
||||
...new Set(
|
||||
[
|
||||
dataModule.value.resolvedKeyField,
|
||||
...listConfig.value.map((item) => fieldById.get(String(item.b_field_id))?.b_field),
|
||||
].filter(Boolean),
|
||||
),
|
||||
];
|
||||
const response = await pageDataApi({
|
||||
viewName: dataModule.value.b_viewtable,
|
||||
orderBy: resolveOrderBy(),
|
||||
searchColumns,
|
||||
searchCondition: appliedSearchCondition.value,
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
if (disposed || version !== requestVersion || dataVersion !== dataRequestVersion) return;
|
||||
dataSource.value = response.data?.rows || [];
|
||||
pagination.total = Number(response.data?.total ?? response.data?.row_count) || 0;
|
||||
} catch (error) {
|
||||
if (disposed || version !== requestVersion || dataVersion !== dataRequestVersion) return;
|
||||
dataSource.value = [];
|
||||
pagination.total = 0;
|
||||
dataError.value = error?.message || String(error);
|
||||
} finally {
|
||||
if (version === requestVersion && dataVersion === dataRequestVersion) {
|
||||
dataLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
const version = ++requestVersion;
|
||||
dataRequestVersion += 1;
|
||||
dataLoading.value = false;
|
||||
pagination.current = 1;
|
||||
dataSource.value = [];
|
||||
pagination.total = 0;
|
||||
try {
|
||||
const loaded = await loadModuleConfig(version);
|
||||
if (loaded) await fetchData(version);
|
||||
} catch {
|
||||
dataSource.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function search({ entries = [] }) {
|
||||
commitAppliedFilters(entries.map(({ item, value }) => createAppliedFilter(item, value)));
|
||||
pagination.current = 1;
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
commitAppliedFilters([]);
|
||||
pagination.current = 1;
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
function removeFilter(fieldName) {
|
||||
commitAppliedFilters(appliedFilters.value.filter((filter) => filter.key !== fieldName));
|
||||
pagination.current = 1;
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
function changePage(page, pageSize) {
|
||||
pagination.current = pageSize === pagination.pageSize ? page : 1;
|
||||
pagination.pageSize = pageSize;
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
function reload() {
|
||||
return fetchData();
|
||||
}
|
||||
|
||||
function resolveRowKey(row) {
|
||||
const keyField = dataModule.value?.resolvedKeyField;
|
||||
return keyField ? row?.[keyField] : JSON.stringify(row);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [String(unref(options.dataCode) || route.meta.dataCode || ""), route.path],
|
||||
initialize,
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disposed = true;
|
||||
requestVersion += 1;
|
||||
dataRequestVersion += 1;
|
||||
});
|
||||
|
||||
return {
|
||||
pageModule,
|
||||
dataModule,
|
||||
activeFields,
|
||||
listConfig,
|
||||
queryItems,
|
||||
dataSource,
|
||||
appliedFilters,
|
||||
pagination,
|
||||
configLoading,
|
||||
dataLoading,
|
||||
loading,
|
||||
configError,
|
||||
dataError,
|
||||
initialize,
|
||||
search,
|
||||
resetSearch,
|
||||
removeFilter,
|
||||
changePage,
|
||||
reload,
|
||||
resolveRowKey,
|
||||
};
|
||||
}
|
||||
@@ -146,6 +146,16 @@ const dragRowColumn = {
|
||||
resizable: false,
|
||||
};
|
||||
|
||||
function getHeaderAlign(column) {
|
||||
if (column.headerAlign) return column.headerAlign;
|
||||
if (isSelectionColumn(column)) return "center";
|
||||
if (column.dataIndex === "actions") return "center";
|
||||
if (column.actions && column.actions.length) return "center";
|
||||
if (column.checkbox === true) return "center";
|
||||
if (column.editor && column.editor.type === "checkbox") return "center";
|
||||
return "left";
|
||||
}
|
||||
|
||||
const displayColumns = computed(() => {
|
||||
const cols = props.columns.map((col) => resolveCustomCell(col, editingCtx));
|
||||
if (props.rowDraggable) cols.unshift(dragRowColumn);
|
||||
@@ -160,7 +170,10 @@ const displayColumns = computed(() => {
|
||||
resizable: false,
|
||||
});
|
||||
}
|
||||
return cols;
|
||||
return cols.map((column) => ({
|
||||
...column,
|
||||
headerAlign: getHeaderAlign(column),
|
||||
}));
|
||||
});
|
||||
|
||||
// 剥离内部列(选择列、序号列、拖拽列)后回传,避免污染父组件 columns
|
||||
|
||||
Reference in new issue
Block a user