20260728225443
This commit is contained in:
1 parent
5ec7aadef9
commit
f6b79ea09a
30 files changed
+1541
-1120
No files matched your search
@@ -60,18 +60,7 @@ GO
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_module_canuse DEFAULT (1),
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_module_xh DEFAULT (0),
|
||||
CONSTRAINT PK_s_module PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_module_code UNIQUE (b_code),
|
||||
CONSTRAINT CK_s_module_code CHECK (
|
||||
LEN(b_code) BETWEEN 2 AND 50
|
||||
AND b_code COLLATE Latin1_General_100_BIN2 LIKE '[a-z]%'
|
||||
AND b_code COLLATE Latin1_General_100_BIN2 NOT LIKE '%[^a-z0-9_]%'
|
||||
),
|
||||
CONSTRAINT CK_s_module_type CHECK (b_module_type IN ('directory', 'page', 'data')),
|
||||
CONSTRAINT CK_s_module_canmenu CHECK (b_canmenu IN (0, 1)),
|
||||
CONSTRAINT CK_s_module_canuse CHECK (b_canuse IN (0, 1)),
|
||||
CONSTRAINT CK_s_module_directory CHECK (b_module_type <> 'directory' OR (b_route IS NULL AND b_viewtable IS NULL AND b_savetable IS NULL)),
|
||||
CONSTRAINT CK_s_module_page CHECK (b_module_type <> 'page' OR b_route IS NOT NULL),
|
||||
CONSTRAINT CK_s_module_data CHECK (b_module_type <> 'data' OR (b_canmenu = 0 AND b_route IS NULL))
|
||||
CONSTRAINT UQ_s_module_code UNIQUE (b_code)
|
||||
);
|
||||
CREATE UNIQUE INDEX UX_s_module_route ON dbo.s_module (b_route) WHERE b_route IS NOT NULL;
|
||||
CREATE INDEX IX_s_module_parent ON dbo.s_module (b_parent_id, b_xh, b_id);
|
||||
@@ -87,10 +76,7 @@ GO
|
||||
b_options nvarchar(max) NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_module_field_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_s_module_field PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_module_field UNIQUE (b_module_id, b_field),
|
||||
CONSTRAINT CK_s_module_field_type CHECK (b_type IN ('input', 'textarea', 'number', 'money', 'date', 'datetime', 'switch', 'select')),
|
||||
CONSTRAINT CK_s_module_field_canuse CHECK (b_canuse IN (0, 1)),
|
||||
CONSTRAINT CK_s_module_field_options CHECK (b_options IS NULL OR ISJSON(b_options) = 1)
|
||||
CONSTRAINT UQ_s_module_field UNIQUE (b_module_id, b_field)
|
||||
);
|
||||
CREATE INDEX IX_s_module_field_module ON dbo.s_module_field (b_module_id, b_canuse, b_id);
|
||||
|
||||
@@ -102,8 +88,7 @@ GO
|
||||
b_i18n varchar(150) NULL,
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_module_field_group_xh DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_module_field_group_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_s_module_field_group PRIMARY KEY (b_id),
|
||||
CONSTRAINT CK_s_module_field_group_canuse CHECK (b_canuse IN (0, 1))
|
||||
CONSTRAINT PK_s_module_field_group PRIMARY KEY (b_id)
|
||||
);
|
||||
CREATE INDEX IX_s_module_field_group_module ON dbo.s_module_field_group (b_module_id, b_canuse, b_xh, b_id);
|
||||
|
||||
@@ -116,9 +101,7 @@ GO
|
||||
b_visible int NOT NULL CONSTRAINT DF_s_module_field_list_visible DEFAULT (1),
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_module_field_list_xh DEFAULT (0),
|
||||
b_width int NULL,
|
||||
CONSTRAINT PK_s_module_field_list PRIMARY KEY (b_id),
|
||||
CONSTRAINT CK_s_module_field_list_visible CHECK (b_visible IN (0, 1)),
|
||||
CONSTRAINT CK_s_module_field_list_width CHECK (b_width IS NULL OR b_width > 0)
|
||||
CONSTRAINT PK_s_module_field_list PRIMARY KEY (b_id)
|
||||
);
|
||||
CREATE UNIQUE INDEX UX_s_module_field_list_field ON dbo.s_module_field_list (b_module_id, b_field_id) WHERE b_field_id IS NOT NULL;
|
||||
CREATE INDEX IX_s_module_field_list_tree ON dbo.s_module_field_list (b_module_id, b_group_id, b_xh, b_id);
|
||||
@@ -133,10 +116,7 @@ GO
|
||||
b_group_id bigint NULL,
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_module_field_edit_xh DEFAULT (0),
|
||||
CONSTRAINT PK_s_module_field_edit PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_module_field_edit UNIQUE (b_module_id, b_field_id),
|
||||
CONSTRAINT CK_s_module_field_edit_required CHECK (b_required IN (0, 1)),
|
||||
CONSTRAINT CK_s_module_field_edit_readonly CHECK (b_readonly IN (0, 1)),
|
||||
CONSTRAINT CK_s_module_field_edit_disabled CHECK (b_disabled IN (0, 1))
|
||||
CONSTRAINT UQ_s_module_field_edit UNIQUE (b_module_id, b_field_id)
|
||||
);
|
||||
CREATE INDEX IX_s_module_field_edit_module ON dbo.s_module_field_edit (b_module_id, b_xh, b_id);
|
||||
|
||||
@@ -150,9 +130,7 @@ GO
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_module_field_query_xh DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_module_field_query_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_s_module_field_query PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_module_field_query UNIQUE (b_module_id, b_field_id),
|
||||
CONSTRAINT CK_s_module_field_query_operator CHECK (b_operator IN ('=', 'like', 'between', 'in')),
|
||||
CONSTRAINT CK_s_module_field_query_canuse CHECK (b_canuse IN (0, 1))
|
||||
CONSTRAINT UQ_s_module_field_query UNIQUE (b_module_id, b_field_id)
|
||||
);
|
||||
CREATE INDEX IX_s_module_field_query_module ON dbo.s_module_field_query (b_module_id, b_canuse, b_xh, b_id);
|
||||
|
||||
@@ -165,8 +143,7 @@ GO
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_module_power_xh DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_module_power_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_s_module_power PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_module_power UNIQUE (b_module_id, b_code),
|
||||
CONSTRAINT CK_s_module_power_canuse CHECK (b_canuse IN (0, 1))
|
||||
CONSTRAINT UQ_s_module_power UNIQUE (b_module_id, b_code)
|
||||
);
|
||||
|
||||
CREATE TABLE dbo.b_user_module (
|
||||
|
||||
@@ -34,9 +34,7 @@ BEGIN TRANSACTION;
|
||||
b_default int NOT NULL CONSTRAINT DF_b_i18n_type_default DEFAULT (0),
|
||||
b_xh int NOT NULL CONSTRAINT DF_b_i18n_type_xh DEFAULT (0),
|
||||
CONSTRAINT PK_b_i18n_type PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_b_i18n_type_code UNIQUE (b_code),
|
||||
CONSTRAINT CK_b_i18n_type_canuse CHECK (b_canuse IN (0, 1)),
|
||||
CONSTRAINT CK_b_i18n_type_default CHECK (b_default IN (0, 1))
|
||||
CONSTRAINT UQ_b_i18n_type_code UNIQUE (b_code)
|
||||
);
|
||||
CREATE INDEX IX_b_i18n_type_xh ON dbo.b_i18n_type (b_xh, b_id);
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* 删除数据库中的 CHECK 约束。 */
|
||||
DECLARE @Sql nvarchar(max) = N'';
|
||||
|
||||
SELECT @Sql = @Sql
|
||||
+ N'ALTER TABLE '
|
||||
+ QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + N'.'
|
||||
+ QUOTENAME(OBJECT_NAME(parent_object_id))
|
||||
+ N' DROP CONSTRAINT ' + QUOTENAME(name) + N';'
|
||||
FROM sys.check_constraints;
|
||||
|
||||
IF @Sql <> N''
|
||||
EXEC sys.sp_executesql @Sql;
|
||||
|
||||
/* 将旧的 switch 字段类型统一改为 checkbox。 */
|
||||
IF OBJECT_ID(N'dbo.s_module_field', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
UPDATE dbo.s_module_field
|
||||
SET b_type = 'checkbox'
|
||||
WHERE b_type = 'switch';
|
||||
END;
|
||||
@@ -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
|
||||
|
||||
@@ -12,22 +12,52 @@ const permissionStore = usePermissionStore();
|
||||
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
|
||||
const openKeys = ref(["system"]);
|
||||
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
|
||||
function moduleLabel(moduleId, path, fallback) {
|
||||
const module = permissionStore.modules.find(
|
||||
(item) => item.b_code === moduleId || item.b_route === path,
|
||||
);
|
||||
return appStore.t(module?.b_i18n, fallback);
|
||||
}
|
||||
function directoryLabel(fallback) {
|
||||
const module = permissionStore.modules.find(
|
||||
(item) => item.b_module_type === "directory" && item.b_name === fallback,
|
||||
);
|
||||
return appStore.t(module?.b_i18n, fallback);
|
||||
}
|
||||
function canAccessMenu(item) {
|
||||
if (item.moduleId) return permissionStore.canAccess(item.moduleId);
|
||||
return true;
|
||||
}
|
||||
const menuItems = computed(() => {
|
||||
const systemPages = [
|
||||
{ key: "/system/module-management", label: "模块管理", moduleId: "m_metadata" },
|
||||
{ key: "/system/i18n-type", label: "多语言类型", moduleId: "b_i18n_type" },
|
||||
{ key: "/system/login-log", label: "登录日志", moduleId: "d_login_log" },
|
||||
{
|
||||
key: "/system/module-management",
|
||||
label: moduleLabel("m_metadata", "/system/module-management", "模块管理"),
|
||||
moduleId: "m_metadata",
|
||||
},
|
||||
{
|
||||
key: "/system/i18n-type",
|
||||
label: moduleLabel("b_i18n_type", "/system/i18n-type", "多语言类型"),
|
||||
moduleId: "b_i18n_type",
|
||||
},
|
||||
{
|
||||
key: "/system/login-log",
|
||||
label: moduleLabel("d_login_log", "/system/login-log", "登录日志"),
|
||||
moduleId: "d_login_log",
|
||||
},
|
||||
].filter(canAccessMenu);
|
||||
const items = [{ key: "/dashboard", icon: menuIcon(House), label: "工作台" }];
|
||||
const items = [
|
||||
{
|
||||
key: "/dashboard",
|
||||
icon: menuIcon(House),
|
||||
label: appStore.t("common.dashboard", "工作台"),
|
||||
},
|
||||
];
|
||||
if (systemPages.length)
|
||||
items.push({
|
||||
key: "system",
|
||||
icon: menuIcon(Settings),
|
||||
label: "系统管理",
|
||||
label: directoryLabel("系统管理"),
|
||||
children: systemPages,
|
||||
});
|
||||
return items;
|
||||
@@ -59,7 +89,7 @@ onMounted(() => permissionStore.load().catch(() => {}));
|
||||
>
|
||||
<div class="brand" :class="{ 'brand--collapsed': effectiveCollapsed }">
|
||||
<div class="brand__mark">F</div>
|
||||
<strong>FMS 管理系统</strong>
|
||||
<strong>{{ appStore.t("common.appName", "FMS 管理系统") }}</strong>
|
||||
</div>
|
||||
<div class="sider-menu">
|
||||
<a-menu
|
||||
@@ -72,7 +102,13 @@ onMounted(() => permissionStore.load().catch(() => {}));
|
||||
/>
|
||||
</div>
|
||||
<div class="sider-footer">
|
||||
<a-tooltip :title="effectiveCollapsed ? '展开侧栏' : '收起侧栏'" placement="right"
|
||||
<a-tooltip
|
||||
:title="
|
||||
effectiveCollapsed
|
||||
? appStore.t('common.expandSidebar', '展开侧栏')
|
||||
: appStore.t('common.collapseSidebar', '收起侧栏')
|
||||
"
|
||||
placement="right"
|
||||
><a-button type="text" class="sider-collapse" @click="appStore.toggleSidebar"
|
||||
><PanelLeftOpen v-if="effectiveCollapsed" /><PanelLeftClose v-else /></a-button
|
||||
></a-tooltip>
|
||||
|
||||
@@ -13,10 +13,12 @@ import {
|
||||
} from "@lucide/vue";
|
||||
import { theme } from "antdv-next";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const { token } = theme.useToken();
|
||||
const headerRef = ref(null);
|
||||
const trackRef = ref(null);
|
||||
@@ -34,6 +36,7 @@ watch(
|
||||
appStore.addVisitedTab({
|
||||
key: route.fullPath,
|
||||
label: getTabLabel(),
|
||||
i18nKey: getTabI18nKey(),
|
||||
closable: route.fullPath !== "/dashboard",
|
||||
});
|
||||
}
|
||||
@@ -74,6 +77,18 @@ function getTabLabel() {
|
||||
return route.meta.title;
|
||||
}
|
||||
|
||||
function getTabI18nKey() {
|
||||
if (route.path === "/dashboard") return "common.dashboard";
|
||||
const module = permissionStore.modules.find(
|
||||
(item) => item.b_code === route.meta.moduleId || item.b_route === route.path,
|
||||
);
|
||||
return module?.b_i18n || "";
|
||||
}
|
||||
|
||||
function getDisplayTabLabel(item) {
|
||||
return appStore.t(item.i18nKey, item.label);
|
||||
}
|
||||
|
||||
function closeTab(targetKey) {
|
||||
if (typeof targetKey !== "string" || targetKey === "/dashboard") return;
|
||||
|
||||
@@ -226,7 +241,7 @@ function toggleMaximize() {
|
||||
@click="changeTab(item.key)"
|
||||
@keydown="handleTabKeydown($event, index)"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<span>{{ getDisplayTabLabel(item) }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.closable"
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<script setup>
|
||||
import { computed, h, ref, watch } from "vue";
|
||||
import { computed, h, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { message } from "antdv-next";
|
||||
import { Bell, ChevronDown, Languages, LogOut, Settings, UserRound } from "@lucide/vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
const authStore = useAuthStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const profileOpen = ref(false);
|
||||
const logoutOpen = ref(false);
|
||||
const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
|
||||
@@ -16,35 +19,74 @@ const menuIcon = (icon) => () => h(icon, { size: 16, strokeWidth: 1.8 });
|
||||
const breadcrumbItems = computed(() => {
|
||||
const items = [];
|
||||
|
||||
if (route.path === "/dashboard") return [{ label: "工作台" }];
|
||||
if (route.meta.section) items.push({ label: route.meta.section });
|
||||
if (route.meta.title) items.push({ label: route.meta.title });
|
||||
if (route.path === "/dashboard") {
|
||||
return [{ label: appStore.t("common.dashboard", "工作台") }];
|
||||
}
|
||||
if (route.meta.section) {
|
||||
const directory = permissionStore.modules.find(
|
||||
(item) => item.b_module_type === "directory" && item.b_name === route.meta.section,
|
||||
);
|
||||
items.push({ label: appStore.t(directory?.b_i18n, route.meta.section) });
|
||||
}
|
||||
if (route.meta.title) {
|
||||
const module = permissionStore.modules.find(
|
||||
(item) => item.b_code === route.meta.moduleId || item.b_route === route.path,
|
||||
);
|
||||
items.push({ label: appStore.t(module?.b_i18n, route.meta.title) });
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
const languageMenu = computed(() => ({
|
||||
items: [
|
||||
{ key: "zh-CN", label: "简体中文" },
|
||||
{ key: "en", label: "English" },
|
||||
],
|
||||
onClick: ({ key }) => appStore.setLocale(key),
|
||||
items: appStore.languages.map((item) => ({
|
||||
key: String(item.b_code),
|
||||
label: item.b_name || item.b_code,
|
||||
})),
|
||||
onClick: async ({ key }) => {
|
||||
try {
|
||||
await appStore.setLocale(key);
|
||||
} catch (error) {
|
||||
message.error("切换语言失败:" + (error?.message || error));
|
||||
}
|
||||
},
|
||||
selectable: true,
|
||||
selectedKeys: [appStore.locale],
|
||||
}));
|
||||
|
||||
const userMenu = {
|
||||
const userMenu = computed(() => ({
|
||||
items: [
|
||||
{ key: "profile", icon: menuIcon(UserRound), label: "个人资料" },
|
||||
{ key: "settings", icon: menuIcon(Settings), label: "账户设置" },
|
||||
{
|
||||
key: "profile",
|
||||
icon: menuIcon(UserRound),
|
||||
label: appStore.t("common.profile", "个人资料"),
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
icon: menuIcon(Settings),
|
||||
label: appStore.t("common.accountSettings", "账户设置"),
|
||||
},
|
||||
{ type: "divider" },
|
||||
{ key: "logout", icon: menuIcon(LogOut), label: "退出登录", danger: true },
|
||||
{
|
||||
key: "logout",
|
||||
icon: menuIcon(LogOut),
|
||||
label: appStore.t("common.logout", "退出登录"),
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
onClick: handleUserMenu,
|
||||
};
|
||||
}));
|
||||
|
||||
const displayName = computed(() => authStore.user?.name || authStore.user?.id || "当前用户");
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await appStore.loadLanguages();
|
||||
} catch (error) {
|
||||
message.error("加载语言配置失败:" + (error?.message || error));
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
(locale) => document.documentElement.setAttribute("lang", locale),
|
||||
@@ -72,15 +114,30 @@ function confirmLogout() {
|
||||
</a-breadcrumb>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<a-tooltip title="切换语言" placement="bottom">
|
||||
<a-dropdown :menu="languageMenu" :trigger="['click']" placement="bottomRight">
|
||||
<a-button type="text" class="toolbar-action" aria-label="切换语言">
|
||||
<a-tooltip :title="appStore.t('common.switchLanguage', '切换语言')" placement="bottom">
|
||||
<a-dropdown
|
||||
:menu="languageMenu"
|
||||
:disabled="
|
||||
appStore.languagesLoading || appStore.translationLoading || !appStore.languages.length
|
||||
"
|
||||
:trigger="['click']"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<a-button
|
||||
type="text"
|
||||
class="toolbar-action"
|
||||
:aria-label="appStore.t('common.switchLanguage', '切换语言')"
|
||||
>
|
||||
<template #icon><Languages /></template>
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="通知提醒" placement="bottom">
|
||||
<a-button type="text" class="toolbar-action" aria-label="通知提醒">
|
||||
<a-tooltip :title="appStore.t('common.notifications', '通知提醒')" placement="bottom">
|
||||
<a-button
|
||||
type="text"
|
||||
class="toolbar-action"
|
||||
:aria-label="appStore.t('common.notifications', '通知提醒')"
|
||||
>
|
||||
<a-badge dot><Bell /></a-badge>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { loadDataApi } from "@/services/api";
|
||||
|
||||
const createDefaultVisitedTabs = () => [{ key: "/dashboard", label: "工作台", closable: false }];
|
||||
const createDefaultVisitedTabs = () => [
|
||||
{ key: "/dashboard", label: "工作台", i18nKey: "common.dashboard", closable: false },
|
||||
];
|
||||
|
||||
export const useAppStore = defineStore(
|
||||
"app",
|
||||
() => {
|
||||
const locale = ref("zh-CN");
|
||||
const locale = ref("");
|
||||
const languages = ref([]);
|
||||
const languagesLoading = ref(false);
|
||||
const translations = ref({});
|
||||
const translationLoading = ref(false);
|
||||
const sidebarCollapsed = ref(false);
|
||||
const visitedTabs = ref(createDefaultVisitedTabs());
|
||||
const contentMaximized = ref(false);
|
||||
@@ -29,8 +36,71 @@ export const useAppStore = defineStore(
|
||||
return routeRefreshKeys.value[String(routeKey || "")] || 0;
|
||||
}
|
||||
|
||||
function setLocale(value) {
|
||||
locale.value = value;
|
||||
async function loadLanguages() {
|
||||
if (languagesLoading.value) return;
|
||||
languagesLoading.value = true;
|
||||
try {
|
||||
const response = await loadDataApi(
|
||||
"b_i18n_type",
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
[],
|
||||
);
|
||||
languages.value = response.data || [];
|
||||
|
||||
let selectedLanguage = languages.value.find(
|
||||
(item) => String(item.b_code) === String(locale.value),
|
||||
);
|
||||
if (!selectedLanguage) {
|
||||
const defaultLanguage = languages.value.find((item) => Number(item.b_default) === 1);
|
||||
selectedLanguage = defaultLanguage || languages.value[0];
|
||||
}
|
||||
|
||||
const code = String(selectedLanguage?.b_code || "");
|
||||
await loadTranslations(code);
|
||||
locale.value = code;
|
||||
} finally {
|
||||
languagesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTranslations(code) {
|
||||
if (!code) {
|
||||
translations.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
translationLoading.value = true;
|
||||
try {
|
||||
const escapedCode = String(code).replaceAll("'", "''");
|
||||
const response = await loadDataApi(
|
||||
"b_i18n",
|
||||
`b_locale = N'${escapedCode}'`,
|
||||
"b_key ASC",
|
||||
[],
|
||||
);
|
||||
const values = {};
|
||||
for (const item of response.data || []) {
|
||||
if (item.b_key) values[item.b_key] = item.b_value;
|
||||
}
|
||||
translations.value = values;
|
||||
} finally {
|
||||
translationLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setLocale(value) {
|
||||
const code = String(value || "");
|
||||
const exists = languages.value.some((item) => String(item.b_code) === code);
|
||||
if (!exists) return;
|
||||
await loadTranslations(code);
|
||||
locale.value = code;
|
||||
}
|
||||
|
||||
function t(key, fallback = "") {
|
||||
const value = translations.value[String(key || "")];
|
||||
if (value !== undefined && value !== null && value !== "") return value;
|
||||
return fallback || String(key || "");
|
||||
}
|
||||
|
||||
function addVisitedTab(tab) {
|
||||
@@ -46,6 +116,10 @@ export const useAppStore = defineStore(
|
||||
|
||||
return {
|
||||
locale,
|
||||
languages,
|
||||
languagesLoading,
|
||||
translations,
|
||||
translationLoading,
|
||||
sidebarCollapsed,
|
||||
visitedTabs,
|
||||
contentMaximized,
|
||||
@@ -54,6 +128,9 @@ export const useAppStore = defineStore(
|
||||
refreshRoute,
|
||||
getRouteRefreshKey,
|
||||
setLocale,
|
||||
loadLanguages,
|
||||
loadTranslations,
|
||||
t,
|
||||
addVisitedTab,
|
||||
removeVisitedTab,
|
||||
};
|
||||
|
||||
@@ -127,6 +127,7 @@ export const usePermissionStore = defineStore("permissions", () => {
|
||||
canAccess,
|
||||
canPower,
|
||||
clear,
|
||||
modules,
|
||||
grantedPageIds,
|
||||
moduleCodes,
|
||||
powerCodes,
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
export const I18N_LOCALES = [
|
||||
{ value: "zh-CN", label: "简体中文", shortLabel: "简体" },
|
||||
{ value: "en-US", label: "English", shortLabel: "English" },
|
||||
{ value: "zh-TW", label: "繁體中文", shortLabel: "繁體" },
|
||||
];
|
||||
|
||||
export const I18N_LOCALE_CODES = I18N_LOCALES.map((item) => item.value);
|
||||
|
||||
// 与数据库 s_module 的 CHECK 约束 CK_s_module_code 保持一致:
|
||||
// 长度 2-50,以小写字母开头,仅含小写字母/数字/下划线。
|
||||
export function validateModuleCode(code) {
|
||||
const value = String(code ?? "").trim();
|
||||
if (!value) return "编码不能为空";
|
||||
|
||||
@@ -44,9 +44,12 @@ const treeData = computed(() =>
|
||||
}),
|
||||
);
|
||||
|
||||
const selectedGroup = computed(() =>
|
||||
localGroups.value.find((g) => String(g.b_id) === selectedGroupId.value),
|
||||
);
|
||||
const selectedGroup = computed(() => {
|
||||
for (const group of localGroups.value) {
|
||||
if (String(group.b_id) === selectedGroupId.value) return group;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function onTreeSelect(keys) {
|
||||
if (keys.length > 0) {
|
||||
@@ -72,14 +75,25 @@ function addGroup(parentId) {
|
||||
|
||||
function deleteGroup() {
|
||||
if (!selectedGroupId.value) return;
|
||||
const hasChildren = localGroups.value.some(
|
||||
(group) => String(group.b_parent_id) === selectedGroupId.value,
|
||||
);
|
||||
let hasChildren = false;
|
||||
for (const group of localGroups.value) {
|
||||
if (String(group.b_parent_id) === selectedGroupId.value) {
|
||||
hasChildren = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasChildren) {
|
||||
message.warning("该分组下还有子分组,请先删除子分组");
|
||||
return;
|
||||
}
|
||||
if (props.referencedGroupIds.map(String).includes(selectedGroupId.value)) {
|
||||
let isReferenced = false;
|
||||
for (const groupId of props.referencedGroupIds) {
|
||||
if (String(groupId) === selectedGroupId.value) {
|
||||
isReferenced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isReferenced) {
|
||||
message.warning("该分组仍被列表或编辑配置引用,请先清空引用");
|
||||
return;
|
||||
}
|
||||
@@ -88,7 +102,14 @@ function deleteGroup() {
|
||||
}
|
||||
|
||||
async function saveAllGroups() {
|
||||
if (localGroups.value.some((group) => !String(group.b_title || "").trim())) {
|
||||
let hasEmptyTitle = false;
|
||||
for (const group of localGroups.value) {
|
||||
if (!String(group.b_title || "").trim()) {
|
||||
hasEmptyTitle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasEmptyTitle) {
|
||||
message.warning("请填写分组标题");
|
||||
return;
|
||||
}
|
||||
@@ -161,7 +182,7 @@ function onClose() {
|
||||
<a-input-number v-model:value="selectedGroup.b_xh" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch
|
||||
<a-checkbox
|
||||
:checked="selectedGroup.b_canuse === 1"
|
||||
@update:checked="(v) => (selectedGroup.b_canuse = v ? 1 : 0)"
|
||||
/>
|
||||
|
||||
@@ -2,30 +2,18 @@
|
||||
import { ref } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { loadDataApi, saveObjectApi } from "@/services/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
// 快速设置多语言弹窗:按 key 加载 b_i18n 各语言值,保存后 emit("saved", key)。
|
||||
// 调用方通过 ref 调用 open(key, defaultValue),defaultValue 用于默认语言的初始值。
|
||||
|
||||
const emit = defineEmits(["saved"]);
|
||||
const appStore = useAppStore();
|
||||
|
||||
const visible = ref(false);
|
||||
const saving = ref(false);
|
||||
const i18nKey = ref("");
|
||||
const entries = ref([]);
|
||||
const langTypes = ref([]);
|
||||
|
||||
async function loadLangTypes() {
|
||||
try {
|
||||
const res = await loadDataApi(
|
||||
"b_i18n_type",
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
langTypes.value = res.data || [];
|
||||
} catch (e) {
|
||||
message.error("加载语言类型失败:" + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
async function open(key, defaultValue = "") {
|
||||
if (!key) {
|
||||
@@ -35,22 +23,38 @@ async function open(key, defaultValue = "") {
|
||||
i18nKey.value = key;
|
||||
const escaped = key.replace(/'/g, "''");
|
||||
try {
|
||||
const [i18nRes] = await Promise.all([
|
||||
const responses = await Promise.all([
|
||||
loadDataApi("b_i18n", `b_key = '${escaped}'`, "b_locale ASC"),
|
||||
loadLangTypes(),
|
||||
loadDataApi("b_i18n_type", "b_canuse = 1", "b_default DESC, b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
const i18nRes = responses[0];
|
||||
const langRes = responses[1];
|
||||
const rows = i18nRes.data || [];
|
||||
const byLocale = Object.fromEntries(rows.map((r) => [r.b_locale, r]));
|
||||
entries.value = langTypes.value.map((t) => {
|
||||
const ex = byLocale[t.b_code];
|
||||
const isDefault = t.b_default === 1 || t.b_code === "zh-CN";
|
||||
return {
|
||||
b_id: ex ? ex.b_id : null,
|
||||
b_locale: t.b_code,
|
||||
b_name: t.b_name,
|
||||
b_value: ex ? ex.b_value : isDefault ? defaultValue : "",
|
||||
};
|
||||
});
|
||||
const languages = langRes.data || [];
|
||||
const byLocale = {};
|
||||
for (const row of rows) {
|
||||
byLocale[row.b_locale] = row;
|
||||
}
|
||||
|
||||
const defaultLanguage = languages.find((item) => Number(item.b_default) === 1);
|
||||
const defaultLocale = String(defaultLanguage?.b_code || languages[0]?.b_code || "");
|
||||
const values = [];
|
||||
for (const language of languages) {
|
||||
const existing = byLocale[language.b_code];
|
||||
let value = "";
|
||||
if (existing) {
|
||||
value = existing.b_value;
|
||||
} else if (language.b_code === defaultLocale) {
|
||||
value = defaultValue;
|
||||
}
|
||||
values.push({
|
||||
b_id: existing ? existing.b_id : null,
|
||||
b_locale: language.b_code,
|
||||
b_name: language.b_name,
|
||||
b_value: value,
|
||||
});
|
||||
}
|
||||
entries.value = values;
|
||||
} catch {
|
||||
entries.value = [];
|
||||
}
|
||||
@@ -66,7 +70,7 @@ async function save() {
|
||||
try {
|
||||
const req = { table: "b_i18n", key_field: "b_id", inserts: [], updates: [] };
|
||||
for (const e of entries.value) {
|
||||
const value = e.b_value ?? "";
|
||||
const value = e.b_value == null ? "" : e.b_value;
|
||||
if (!e.b_id && !value) continue;
|
||||
const row = { b_key: i18nKey.value, b_locale: e.b_locale, b_value: value };
|
||||
if (e.b_id) req.updates.push({ ...row, b_id: e.b_id });
|
||||
@@ -74,6 +78,7 @@ async function save() {
|
||||
}
|
||||
if (req.inserts.length || req.updates.length) {
|
||||
await saveObjectApi([req]);
|
||||
if (appStore.locale) await appStore.loadTranslations(appStore.locale);
|
||||
}
|
||||
message.success("多语言已保存");
|
||||
visible.value = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import FmsMemoInput from "@/components/FmsMemoInput.vue";
|
||||
import { buildModuleI18nKey } from "@/utils/i18n";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
|
||||
@@ -24,8 +25,12 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const isPage = computed(() => formData.value?.b_module_type === "page");
|
||||
const isData = computed(() => formData.value?.b_module_type === "data");
|
||||
const isPage = computed(() => {
|
||||
return formData.value && formData.value.b_module_type === "page";
|
||||
});
|
||||
const isData = computed(() => {
|
||||
return formData.value && formData.value.b_module_type === "data";
|
||||
});
|
||||
|
||||
function emitUpdate() {
|
||||
if (formData.value) emit("update", formData.value);
|
||||
@@ -55,10 +60,12 @@ function onModuleTypeChange(v) {
|
||||
const i18nModalRef = ref(null);
|
||||
|
||||
function openI18nModal() {
|
||||
const key =
|
||||
formData.value?.b_i18n ||
|
||||
buildModuleI18nKey(formData.value?.b_module_type, formData.value?.b_code);
|
||||
i18nModalRef.value?.open(key, formData.value?.b_name || "");
|
||||
if (!formData.value || !i18nModalRef.value) return;
|
||||
let key = formData.value.b_i18n;
|
||||
if (!key) {
|
||||
key = buildModuleI18nKey(formData.value.b_module_type, formData.value.b_code);
|
||||
}
|
||||
i18nModalRef.value.open(key, formData.value.b_name || "");
|
||||
}
|
||||
|
||||
function onI18nSaved(key) {
|
||||
@@ -155,9 +162,10 @@ function onI18nSaved(key) {
|
||||
</a-col>
|
||||
<a-col v-if="isData" :span="8">
|
||||
<a-form-item label="默认排序">
|
||||
<a-input
|
||||
:value="formData.b_orderfield"
|
||||
@update:value="(v) => setField('b_orderfield', v)"
|
||||
<FmsMemoInput
|
||||
:model-value="formData.b_orderfield || ''"
|
||||
:disabled="loading"
|
||||
@update:model-value="(v) => setField('b_orderfield', v)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
@@ -22,7 +22,11 @@ watch(
|
||||
() => props.editConfig,
|
||||
(val) => {
|
||||
if (val === localEdit.value) return;
|
||||
localEdit.value = val?.map((r) => ({ ...r })) || [];
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
localEdit.value = rows;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -32,24 +36,31 @@ function emitUpdate() {
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localEdit.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
const sortedRows = [];
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
sortedRows.push({ ...rows[index], b_xh: (index + 1) * 10 });
|
||||
}
|
||||
localEdit.value = sortedRows;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
for (const field of props.fields || []) {
|
||||
if (field.b_canuse !== 1) continue;
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
(props.groups || [])
|
||||
.filter((g) => g.b_canuse === 1)
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
const groupOptions = computed(() => {
|
||||
const options = [];
|
||||
for (const group of props.groups || []) {
|
||||
if (group.b_canuse !== 1) continue;
|
||||
options.push({ label: group.b_title || `分组-${group.b_id}`, value: String(group.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
@@ -57,16 +68,6 @@ function deleteRow(record) {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function makeSwitchCol(field, title, width) {
|
||||
return {
|
||||
dataIndex: field,
|
||||
title,
|
||||
width: width || 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox" },
|
||||
};
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
{
|
||||
dataIndex: "b_field_id",
|
||||
@@ -77,9 +78,27 @@ const columns = shallowRef([
|
||||
options: () => availableFields.value,
|
||||
},
|
||||
},
|
||||
makeSwitchCol("b_required", "必填"),
|
||||
makeSwitchCol("b_readonly", "只读"),
|
||||
makeSwitchCol("b_disabled", "禁用"),
|
||||
{
|
||||
dataIndex: "b_required",
|
||||
title: "必填",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_readonly",
|
||||
title: "只读",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_disabled",
|
||||
title: "禁用",
|
||||
width: 60,
|
||||
align: "center",
|
||||
editor: { type: "checkbox" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_group_id",
|
||||
title: "编辑分组",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { h, ref, watch } from "vue";
|
||||
import { message } from "antdv-next";
|
||||
import { Languages } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
import { buildModuleItemI18nKey, syncModuleItemI18nKeys } from "@/utils/i18n";
|
||||
import I18nQuickSetModal from "./I18nQuickSetModal.vue";
|
||||
|
||||
const FIELD_TYPES = [
|
||||
{ label: "输入框", value: "input" },
|
||||
@@ -10,7 +13,7 @@ const FIELD_TYPES = [
|
||||
{ label: "金额", value: "money" },
|
||||
{ label: "日期", value: "date" },
|
||||
{ label: "日期时间", value: "datetime" },
|
||||
{ label: "开关", value: "switch" },
|
||||
{ label: "复选框", value: "checkbox" },
|
||||
{ label: "下拉选择", value: "select" },
|
||||
];
|
||||
|
||||
@@ -21,16 +24,20 @@ const props = defineProps({
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update"]);
|
||||
const emit = defineEmits(["update", "i18nSaved"]);
|
||||
|
||||
const localFields = ref([]);
|
||||
const activeKey = ref(null);
|
||||
const i18nModalRef = ref(null);
|
||||
|
||||
watch(
|
||||
() => [props.fields, props.moduleCode],
|
||||
([val, moduleCode], previous = []) => {
|
||||
if (val === localFields.value && moduleCode === previous[1]) return;
|
||||
const rows = val?.map((row) => ({ ...row })) || [];
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
const changed = syncModuleItemI18nKeys(rows, "field", moduleCode, "b_field");
|
||||
localFields.value = rows;
|
||||
if (changed) emit("update", rows);
|
||||
@@ -48,6 +55,59 @@ function onCellChange() {
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
function openI18n(row) {
|
||||
const key = buildModuleItemI18nKey("field", props.moduleCode, row.b_field);
|
||||
if (!key) {
|
||||
message.warning("请先填写字段名和模块编码");
|
||||
return;
|
||||
}
|
||||
if (i18nModalRef.value) {
|
||||
i18nModalRef.value.open(key, row.b_name || row.b_field || "");
|
||||
}
|
||||
}
|
||||
|
||||
const I18nKeyCell = ({ row, cellValue }) =>
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
style: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "4px",
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
[
|
||||
h(
|
||||
"span",
|
||||
{ style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } },
|
||||
cellValue || "",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
title: "设置多语言",
|
||||
style: {
|
||||
flex: "none",
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
cursor: "pointer",
|
||||
padding: "2px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
onClick: (event) => {
|
||||
event.stopPropagation();
|
||||
openI18n(row);
|
||||
},
|
||||
},
|
||||
[h(Languages, { size: 14 })],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_field", title: "字段名", width: 140 },
|
||||
{
|
||||
@@ -75,6 +135,7 @@ const columns = ref([
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 220,
|
||||
customCell: I18nKeyCell,
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
@@ -99,6 +160,7 @@ const columns = ref([
|
||||
@change="onCellChange"
|
||||
/>
|
||||
</div>
|
||||
<I18nQuickSetModal ref="i18nModalRef" @saved="(key) => emit('i18nSaved', key)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -24,49 +24,59 @@ const langOptions = ref([]);
|
||||
// 扁平原始数据(b_id, b_key, b_locale, b_value),仅用于保存时差量对比。
|
||||
const originalsFlat = ref([]);
|
||||
// 透视后的行数据:一行一个 key,每种语言的值挂在对应 locale 字段上。
|
||||
// 形如 { _id, b_key, "zh-CN": "…", "en-US": "…", _localeIds: { "zh-CN": b_id, … } }
|
||||
const pivotRows = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const searchKey = ref("");
|
||||
const activeKey = ref(null);
|
||||
const tableRef = ref(null);
|
||||
let loadRequestId = 0;
|
||||
|
||||
const managedItems = computed(() => (props.i18nItems || []).filter((item) => item?.managed));
|
||||
const requestedKeys = computed(() => [
|
||||
...new Set(
|
||||
(props.i18nItems || [])
|
||||
.flatMap((item) => [item?.key, item?.sourceKey])
|
||||
.filter(Boolean)
|
||||
.map(String),
|
||||
),
|
||||
]);
|
||||
const managedItems = computed(() => {
|
||||
const items = [];
|
||||
for (const item of props.i18nItems || []) {
|
||||
if (item && item.managed) items.push(item);
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
const requestedKeys = computed(() => {
|
||||
const keys = new Set();
|
||||
for (const item of props.i18nItems || []) {
|
||||
if (!item) continue;
|
||||
if (item.key) keys.add(String(item.key));
|
||||
if (item.sourceKey) keys.add(String(item.sourceKey));
|
||||
}
|
||||
return Array.from(keys);
|
||||
});
|
||||
|
||||
function isManagedRow(row) {
|
||||
return Boolean(row?._managedId);
|
||||
return Boolean(row && row._managedId);
|
||||
}
|
||||
|
||||
// 保证模块、字段、权限的受管键始终存在。简体中文为空或仍为上一次默认名称时,
|
||||
// 保证模块、字段、权限的受管键始终存在。默认语言为空或仍为上一次默认名称时,
|
||||
// 自动跟随当前名称;人工修改后的翻译不再被名称变化覆盖。
|
||||
// itemsOverride 用于保存时按最终草稿态生成受管键(不传则使用 props.i18nItems)。
|
||||
function reconcileManagedRows(itemsOverride) {
|
||||
const items = itemsOverride || managedItems.value;
|
||||
const rows = [...pivotRows.value];
|
||||
const rowByManagedId = new Map(
|
||||
rows.filter((row) => row._managedId).map((row) => [String(row._managedId), row]),
|
||||
);
|
||||
const unclaimedByKey = new Map(
|
||||
rows.filter((row) => !row._managedId).map((row) => [String(row.b_key), row]),
|
||||
);
|
||||
const rowByManagedId = new Map();
|
||||
const unclaimedByKey = new Map();
|
||||
for (const row of rows) {
|
||||
if (row._managedId) {
|
||||
rowByManagedId.set(String(row._managedId), row);
|
||||
} else {
|
||||
unclaimedByKey.set(String(row.b_key), row);
|
||||
}
|
||||
}
|
||||
const currentManagedIds = new Set();
|
||||
const defaultLocale =
|
||||
langOptions.value.find((item) => item.default)?.value || langOptions.value[0]?.value || "";
|
||||
|
||||
for (const item of items) {
|
||||
const managedId = String(item.id || item.key);
|
||||
currentManagedIds.add(managedId);
|
||||
let row =
|
||||
rowByManagedId.get(managedId) ||
|
||||
unclaimedByKey.get(String(item.key)) ||
|
||||
unclaimedByKey.get(String(item.sourceKey));
|
||||
let row = rowByManagedId.get(managedId);
|
||||
if (!row) row = unclaimedByKey.get(String(item.key));
|
||||
if (!row && item.sourceKey) row = unclaimedByKey.get(String(item.sourceKey));
|
||||
if (!row) {
|
||||
row = { _id: createTempId(), b_key: item.key, _localeIds: {} };
|
||||
rows.push(row);
|
||||
@@ -74,8 +84,13 @@ function reconcileManagedRows(itemsOverride) {
|
||||
|
||||
const previousDefault = row._defaultValue;
|
||||
const nextDefault = item.defaultValue == null ? "" : String(item.defaultValue);
|
||||
if (row["zh-CN"] == null || row["zh-CN"] === "" || row["zh-CN"] === previousDefault) {
|
||||
row["zh-CN"] = nextDefault;
|
||||
if (
|
||||
defaultLocale &&
|
||||
(row[defaultLocale] == null ||
|
||||
row[defaultLocale] === "" ||
|
||||
row[defaultLocale] === previousDefault)
|
||||
) {
|
||||
row[defaultLocale] = nextDefault;
|
||||
}
|
||||
row.b_key = String(item.key);
|
||||
row._managedId = managedId;
|
||||
@@ -103,10 +118,15 @@ async function loadLangTypes() {
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
langOptions.value = (res.data || []).map((t) => ({
|
||||
label: `${t.b_name} (${t.b_code})`,
|
||||
value: t.b_code,
|
||||
}));
|
||||
const options = [];
|
||||
for (const language of res.data || []) {
|
||||
options.push({
|
||||
label: `${language.b_name} (${language.b_code})`,
|
||||
value: language.b_code,
|
||||
default: Number(language.b_default) === 1,
|
||||
});
|
||||
}
|
||||
langOptions.value = options;
|
||||
} catch (e) {
|
||||
message.error("加载语言类型失败:" + (e.message || e));
|
||||
}
|
||||
@@ -132,32 +152,40 @@ function buildPivot(flat) {
|
||||
}
|
||||
|
||||
async function loadI18n() {
|
||||
const requestId = ++loadRequestId;
|
||||
const moduleId = String(props.moduleId);
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const keys = requestedKeys.value;
|
||||
const quotedKeys = keys.map((key) => "'" + key.replace(/'/g, "''") + "'");
|
||||
const where = keys.length ? "b_key IN (" + quotedKeys.join(", ") + ")" : null;
|
||||
const [i18nRes] = await Promise.all([
|
||||
const responses = await Promise.all([
|
||||
where
|
||||
? loadDataApi("b_i18n", where, "b_key ASC, b_locale ASC")
|
||||
: Promise.resolve({ data: [] }),
|
||||
loadLangTypes(),
|
||||
]);
|
||||
const flat = (i18nRes.data || []).map((r) => ({
|
||||
b_id: r.b_id,
|
||||
b_key: r.b_key,
|
||||
b_locale: r.b_locale,
|
||||
b_value: r.b_value,
|
||||
}));
|
||||
if (requestId !== loadRequestId) return;
|
||||
const i18nRes = responses[0];
|
||||
const flat = [];
|
||||
for (const row of i18nRes.data || []) {
|
||||
flat.push({
|
||||
b_id: row.b_id,
|
||||
b_key: row.b_key,
|
||||
b_locale: row.b_locale,
|
||||
b_value: row.b_value,
|
||||
});
|
||||
}
|
||||
if (String(props.moduleId) !== moduleId) return;
|
||||
originalsFlat.value = cloneData(flat);
|
||||
pivotRows.value = buildPivot(flat);
|
||||
reconcileManagedRows();
|
||||
} catch (e) {
|
||||
if (requestId === loadRequestId) message.error("加载多语言失败:" + (e.message || e));
|
||||
if (String(props.moduleId) === moduleId) {
|
||||
message.error("加载多语言失败:" + (e.message || e));
|
||||
}
|
||||
} finally {
|
||||
if (requestId === loadRequestId) isLoading.value = false;
|
||||
if (String(props.moduleId) === moduleId) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,12 +194,15 @@ async function refreshKey(key) {
|
||||
const escaped = String(key).replace(/'/g, "''");
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n", "b_key = '" + escaped + "'", "b_locale ASC");
|
||||
const rows = (res.data || []).map((row) => ({
|
||||
b_id: row.b_id,
|
||||
b_key: row.b_key,
|
||||
b_locale: row.b_locale,
|
||||
b_value: row.b_value,
|
||||
}));
|
||||
const rows = [];
|
||||
for (const row of res.data || []) {
|
||||
rows.push({
|
||||
b_id: row.b_id,
|
||||
b_key: row.b_key,
|
||||
b_locale: row.b_locale,
|
||||
b_value: row.b_value,
|
||||
});
|
||||
}
|
||||
originalsFlat.value = [
|
||||
...originalsFlat.value.filter((row) => row.b_key !== key),
|
||||
...cloneData(rows),
|
||||
@@ -194,7 +225,9 @@ async function addRow() {
|
||||
|
||||
activeKey.value = String(newId);
|
||||
await nextTick();
|
||||
await tableRef.value?.editRow(String(newId), "b_key");
|
||||
if (tableRef.value) {
|
||||
await tableRef.value.editRow(String(newId), "b_key");
|
||||
}
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
@@ -272,7 +305,7 @@ function flatten(rows) {
|
||||
// 由父级「同步字段」调用:把默认翻译条目({ key, locale, value, managedId })并入草稿行。
|
||||
// 已存在同 b_key 的行只补全空值(不覆盖用户配置),避免新增重复键;逻辑见 utils/i18n.mergeI18nDefaults。
|
||||
function mergeDefaults(entries) {
|
||||
if (!entries?.length) return;
|
||||
if (!entries || entries.length === 0) return;
|
||||
pivotRows.value = mergeI18nDefaults(pivotRows.value, entries);
|
||||
}
|
||||
|
||||
@@ -296,7 +329,11 @@ async function loadExistingByKeyLocale(keys) {
|
||||
// 由父级统一保存调用:返回 b_i18n 的变更请求(无变更返回 null),保存成功后父级调用 markSaved 同步基线
|
||||
async function buildSaveRequest() {
|
||||
// 保存涉及的键(受管键可能在新建/改名模块时才生成,打开面板时未被加载)
|
||||
const keys = [...new Set(pivotRows.value.map((r) => String(r.b_key)).filter(Boolean))];
|
||||
const keySet = new Set();
|
||||
for (const row of pivotRows.value) {
|
||||
if (row.b_key) keySet.add(String(row.b_key));
|
||||
}
|
||||
const keys = Array.from(keySet);
|
||||
if (keys.length) {
|
||||
const existing = await loadExistingByKeyLocale(keys);
|
||||
if (existing.size) {
|
||||
@@ -362,7 +399,6 @@ const columns = computed(() => {
|
||||
title: "键",
|
||||
width: 240,
|
||||
align: "left",
|
||||
headerAlign: "left",
|
||||
editor: { type: "input", disabled: (row) => isManagedRow(row) },
|
||||
},
|
||||
];
|
||||
@@ -372,7 +408,6 @@ const columns = computed(() => {
|
||||
title: opt.label,
|
||||
width: 180,
|
||||
align: "left",
|
||||
headerAlign: "left",
|
||||
editor: { type: "input" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ watch(
|
||||
() => props.listConfig,
|
||||
(val) => {
|
||||
if (val === localList.value) return;
|
||||
localList.value = val?.map((r) => ({ ...r })) || [];
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
localList.value = rows;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -32,24 +36,31 @@ function emitUpdate() {
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localList.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
const sortedRows = [];
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
sortedRows.push({ ...rows[index], b_xh: (index + 1) * 10 });
|
||||
}
|
||||
localList.value = sortedRows;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
for (const field of props.fields || []) {
|
||||
if (field.b_canuse !== 1) continue;
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
(props.groups || [])
|
||||
.filter((g) => g.b_canuse === 1)
|
||||
.map((g) => ({ label: g.b_title || `分组-${g.b_id}`, value: String(g.b_id) })),
|
||||
);
|
||||
const groupOptions = computed(() => {
|
||||
const options = [];
|
||||
for (const group of props.groups || []) {
|
||||
if (group.b_canuse !== 1) continue;
|
||||
options.push({ label: group.b_title || `分组-${group.b_id}`, value: String(group.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
function deleteRow(record) {
|
||||
if (props.loading) return;
|
||||
|
||||
@@ -25,7 +25,10 @@ watch(
|
||||
() => [props.powers, props.moduleCode],
|
||||
([val, moduleCode], previous = []) => {
|
||||
if (val === localPowers.value && moduleCode === previous[1]) return;
|
||||
const rows = (val || []).map((row) => ({ ...row }));
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
const changed = syncModuleItemI18nKeys(rows, "power", moduleCode, "b_code");
|
||||
localPowers.value = rows;
|
||||
if (changed) emit("update", rows);
|
||||
@@ -40,7 +43,7 @@ function emitUpdate() {
|
||||
async function addRow() {
|
||||
if (props.loading) return;
|
||||
const newId = createTempId();
|
||||
localPowers.value.push({
|
||||
const newRow = {
|
||||
b_id: newId,
|
||||
b_module_id: props.moduleId,
|
||||
b_code: "",
|
||||
@@ -48,12 +51,17 @@ async function addRow() {
|
||||
b_i18n: null,
|
||||
b_xh: (localPowers.value.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
});
|
||||
};
|
||||
const rows = localPowers.value.slice();
|
||||
rows.push(newRow);
|
||||
localPowers.value = rows;
|
||||
emitUpdate();
|
||||
|
||||
activeKey.value = String(newId);
|
||||
await nextTick();
|
||||
await tableRef.value?.editRow(String(newId), "b_code");
|
||||
if (tableRef.value) {
|
||||
await tableRef.value.editRow(String(newId), "b_code");
|
||||
}
|
||||
}
|
||||
|
||||
function deleteRow(record) {
|
||||
@@ -112,7 +120,9 @@ function openI18n(row) {
|
||||
message.warning("请先填写操作编码");
|
||||
return;
|
||||
}
|
||||
i18nModalRef.value?.open(key, row.b_name || "");
|
||||
if (i18nModalRef.value) {
|
||||
i18nModalRef.value.open(key, row.b_name || "");
|
||||
}
|
||||
}
|
||||
|
||||
// 多语言键单元格:值 + 右侧翻译 icon 按钮(原生 button,避免 antdv Button 组件开销)
|
||||
@@ -159,10 +169,30 @@ const I18nKeyCell = ({ row, cellValue }) =>
|
||||
);
|
||||
|
||||
const columns = ref([
|
||||
{ dataIndex: "b_code", title: "操作编码", width: 160, editor: { type: "input" } },
|
||||
{ dataIndex: "b_name", title: "操作名称", width: 160, editor: { type: "input" } },
|
||||
{ dataIndex: "b_i18n", title: "多语言键", width: 160, customCell: I18nKeyCell },
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80, editor: { type: "input" } },
|
||||
{
|
||||
dataIndex: "b_code",
|
||||
title: "操作编码",
|
||||
width: 160,
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_name",
|
||||
title: "操作名称",
|
||||
width: 160,
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_i18n",
|
||||
title: "多语言键",
|
||||
width: 160,
|
||||
customCell: I18nKeyCell,
|
||||
},
|
||||
{
|
||||
dataIndex: "b_xh",
|
||||
title: "排序",
|
||||
width: 80,
|
||||
editor: { type: "input" },
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "启用",
|
||||
|
||||
+18
-10
@@ -34,7 +34,11 @@ watch(
|
||||
() => props.queryConfig,
|
||||
(val) => {
|
||||
if (val === localQuery.value) return;
|
||||
localQuery.value = val?.map((r) => ({ ...r })) || [];
|
||||
const rows = [];
|
||||
for (const row of val || []) {
|
||||
rows.push({ ...row });
|
||||
}
|
||||
localQuery.value = rows;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -44,18 +48,22 @@ function emitUpdate() {
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
localQuery.value = rows.map((row, index) => ({
|
||||
...row,
|
||||
b_xh: (index + 1) * 10,
|
||||
}));
|
||||
const sortedRows = [];
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
sortedRows.push({ ...rows[index], b_xh: (index + 1) * 10 });
|
||||
}
|
||||
localQuery.value = sortedRows;
|
||||
emitUpdate();
|
||||
}
|
||||
|
||||
const availableFields = computed(() =>
|
||||
(props.fields || [])
|
||||
.filter((f) => f.b_canuse === 1)
|
||||
.map((f) => ({ label: f.b_name || f.b_field, value: String(f.b_id) })),
|
||||
);
|
||||
const availableFields = computed(() => {
|
||||
const options = [];
|
||||
for (const field of props.fields || []) {
|
||||
if (field.b_canuse !== 1) continue;
|
||||
options.push({ label: field.b_name || field.b_field, value: String(field.b_id) });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
function addRow() {
|
||||
if (props.loading) return;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { message, Modal } from "antdv-next";
|
||||
import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue";
|
||||
import FmsTree from "@/components/FmsTree.vue";
|
||||
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from "@/services/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import {
|
||||
cloneData,
|
||||
diffRows,
|
||||
@@ -30,6 +31,7 @@ import ModulePowersPanel from "./components/ModulePowersPanel.vue";
|
||||
import ModuleI18nPanel from "./components/ModuleI18nPanel.vue";
|
||||
|
||||
// ---- 状态 ----
|
||||
const appStore = useAppStore();
|
||||
const treeData = ref([]);
|
||||
const treeLoading = ref(false);
|
||||
const expandedTreeKeys = ref([]);
|
||||
@@ -80,8 +82,6 @@ const configSaving = ref(false);
|
||||
const configSyncing = ref(false);
|
||||
const i18nPanelRef = ref(null);
|
||||
const loadedModuleId = ref(null);
|
||||
let configRequestId = 0;
|
||||
let operationId = 0;
|
||||
|
||||
// 使用函数 ref,避免组件位于 TabPane 的 v-for 内时被 Vue 收集成实例数组。
|
||||
function setI18nPanelRef(instance) {
|
||||
@@ -94,10 +94,6 @@ const busy = computed(() => configLoading.value || configSaving.value || configS
|
||||
const syncDisabled = computed(
|
||||
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
|
||||
);
|
||||
const isCurrentModule = computed(
|
||||
() =>
|
||||
loadedModuleId.value != null && String(loadedModuleId.value) === String(selectedModuleId.value),
|
||||
);
|
||||
// 构建受管多语言项。managedSource 提供模块/字段/权限的受管键来源,
|
||||
// plainSource 提供分组/列表标题等非受管键来源(手动填写,需即时反馈)。
|
||||
function buildI18nItems(managedSource, plainSource) {
|
||||
@@ -245,64 +241,70 @@ async function loadTree() {
|
||||
}
|
||||
|
||||
// ---- 模块配置加载 ----
|
||||
async function loadModuleConfig(moduleId, requestId) {
|
||||
async function loadModuleConfig(moduleId) {
|
||||
configLoading.value = true;
|
||||
try {
|
||||
const id = String(moduleId);
|
||||
const moduleType = findNode(id)?.b_module_type;
|
||||
const requests = [["module", loadDataApi("s_module", `b_id = ${id}`)]];
|
||||
let moduleResponse;
|
||||
let fieldsResponse;
|
||||
let groupsResponse;
|
||||
let listResponse;
|
||||
let editResponse;
|
||||
let queryResponse;
|
||||
let powersResponse;
|
||||
|
||||
if (moduleType === "data") {
|
||||
requests.push(
|
||||
["fields", loadDataApi("s_module_field", `b_module_id = ${id}`, "b_id ASC")],
|
||||
[
|
||||
"groups",
|
||||
loadDataApi("s_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"listConfig",
|
||||
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"editConfig",
|
||||
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
[
|
||||
"queryConfig",
|
||||
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
],
|
||||
);
|
||||
const responses = await Promise.all([
|
||||
loadDataApi("s_module", `b_id = ${id}`),
|
||||
loadDataApi("s_module_field", `b_module_id = ${id}`, "b_id ASC"),
|
||||
loadDataApi("s_module_field_group", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_list", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_edit", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
loadDataApi("s_module_field_query", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
moduleResponse = responses[0];
|
||||
fieldsResponse = responses[1];
|
||||
groupsResponse = responses[2];
|
||||
listResponse = responses[3];
|
||||
editResponse = responses[4];
|
||||
queryResponse = responses[5];
|
||||
} else if (moduleType === "page") {
|
||||
requests.push([
|
||||
"powers",
|
||||
const responses = await Promise.all([
|
||||
loadDataApi("s_module", `b_id = ${id}`),
|
||||
loadDataApi("s_module_power", `b_module_id = ${id}`, "b_xh ASC, b_id ASC"),
|
||||
]);
|
||||
moduleResponse = responses[0];
|
||||
powersResponse = responses[1];
|
||||
} else {
|
||||
moduleResponse = await loadDataApi("s_module", `b_id = ${id}`);
|
||||
}
|
||||
const responses = Object.fromEntries(
|
||||
await Promise.all(requests.map(async ([key, request]) => [key, await request])),
|
||||
);
|
||||
|
||||
if (String(selectedModuleId.value) !== id) return;
|
||||
|
||||
const original = {
|
||||
module: responses.module.data?.[0] || null,
|
||||
fields: responses.fields?.data || [],
|
||||
groups: responses.groups?.data || [],
|
||||
listConfig: responses.listConfig?.data || [],
|
||||
editConfig: responses.editConfig?.data || [],
|
||||
queryConfig: responses.queryConfig?.data || [],
|
||||
powers: responses.powers?.data || [],
|
||||
module: moduleResponse.data?.[0] || null,
|
||||
fields: fieldsResponse ? fieldsResponse.data || [] : [],
|
||||
groups: groupsResponse ? groupsResponse.data || [] : [],
|
||||
listConfig: listResponse ? listResponse.data || [] : [],
|
||||
editConfig: editResponse ? editResponse.data || [] : [],
|
||||
queryConfig: queryResponse ? queryResponse.data || [] : [],
|
||||
powers: powersResponse ? powersResponse.data || [] : [],
|
||||
};
|
||||
if (requestId !== configRequestId) return;
|
||||
originals.value = original;
|
||||
drafts.value = cloneData(original);
|
||||
loadedModuleId.value = id;
|
||||
activeTab.value = "basic";
|
||||
} catch (e) {
|
||||
if (requestId === configRequestId) {
|
||||
if (String(selectedModuleId.value) === String(moduleId)) {
|
||||
loadedModuleId.value = null;
|
||||
drafts.value.module = null;
|
||||
message.error("模块配置加载失败:" + (e.message || e));
|
||||
}
|
||||
} finally {
|
||||
if (requestId === configRequestId) configLoading.value = false;
|
||||
if (String(selectedModuleId.value) === String(moduleId)) {
|
||||
configLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,8 +317,6 @@ function onTreeSelect(keys) {
|
||||
}
|
||||
|
||||
watch(selectedModuleId, (id) => {
|
||||
const requestId = ++configRequestId;
|
||||
operationId++;
|
||||
if (!id) {
|
||||
configLoading.value = false;
|
||||
loadedModuleId.value = null;
|
||||
@@ -329,7 +329,7 @@ watch(selectedModuleId, (id) => {
|
||||
return;
|
||||
}
|
||||
loadedModuleId.value = null;
|
||||
void loadModuleConfig(id, requestId);
|
||||
void loadModuleConfig(id);
|
||||
});
|
||||
|
||||
// ---- 子面板数据变更 ----
|
||||
@@ -345,7 +345,7 @@ function onSetField(field, value) {
|
||||
|
||||
// 分组管理已自行保存到后端,同步更新 originals 和 drafts
|
||||
function onGroupsSaved(newGroups, idMapping) {
|
||||
if (idMapping?.size) {
|
||||
if (idMapping && idMapping.size) {
|
||||
remapForeignKeys(drafts.value.listConfig || [], ["b_group_id"], idMapping);
|
||||
remapForeignKeys(drafts.value.editConfig || [], ["b_group_id"], idMapping);
|
||||
}
|
||||
@@ -354,7 +354,9 @@ function onGroupsSaved(newGroups, idMapping) {
|
||||
}
|
||||
|
||||
function onI18nSaved(key) {
|
||||
void i18nPanelRef.value?.refreshKey?.(key);
|
||||
if (i18nPanelRef.value) {
|
||||
void i18nPanelRef.value.refreshKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 新增模块 ----
|
||||
@@ -442,7 +444,7 @@ function findNode(key) {
|
||||
|
||||
function hasChildNode(moduleId) {
|
||||
const node = findNode(moduleId);
|
||||
return !!node?.children?.length;
|
||||
return Boolean(node && node.children && node.children.length);
|
||||
}
|
||||
|
||||
async function onDeleteNodeById(moduleId) {
|
||||
@@ -486,9 +488,10 @@ async function onDeleteNodeById(moduleId) {
|
||||
|
||||
// ---- 统一保存 ----
|
||||
async function saveAll() {
|
||||
if (!selectedModule.value || !isCurrentModule.value || busy.value) return;
|
||||
if (!selectedModule.value || busy.value) return;
|
||||
if (loadedModuleId.value == null) return;
|
||||
if (String(loadedModuleId.value) !== String(selectedModuleId.value)) return;
|
||||
const moduleId = String(selectedModule.value.b_id);
|
||||
const currentOperationId = ++operationId;
|
||||
configSaving.value = true;
|
||||
|
||||
try {
|
||||
@@ -497,9 +500,13 @@ async function saveAll() {
|
||||
// s_module 主表
|
||||
const moduleData = drafts.value.module;
|
||||
const moduleOriginal = originals.value.module;
|
||||
const moduleCode = String(moduleData?.b_code || "").trim();
|
||||
const moduleCode = String(moduleData.b_code || "").trim();
|
||||
if (!moduleCode) {
|
||||
message.warning(`请填写${moduleData?.b_module_type === "data" ? "模块编码" : "菜单编码"}`);
|
||||
if (moduleData.b_module_type === "data") {
|
||||
message.warning("请填写模块编码");
|
||||
} else {
|
||||
message.warning("请填写菜单编码");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const codeError = validateModuleCode(moduleCode);
|
||||
@@ -507,7 +514,7 @@ async function saveAll() {
|
||||
message.error(`编码格式不正确:${codeError}`);
|
||||
return;
|
||||
}
|
||||
if (!String(moduleData?.b_name || "").trim()) {
|
||||
if (!String(moduleData.b_name || "").trim()) {
|
||||
message.warning("请填写模块名称");
|
||||
return;
|
||||
}
|
||||
@@ -522,13 +529,18 @@ async function saveAll() {
|
||||
syncModuleItemI18nKeys(drafts.value.fields, "field", moduleData.b_code, "b_field");
|
||||
syncModuleItemI18nKeys(drafts.value.powers, "power", moduleData.b_code, "b_code");
|
||||
moduleData.b_i18n = buildModuleI18nKey(moduleData.b_module_type, moduleData.b_code);
|
||||
const isNew = !moduleOriginal?.b_code;
|
||||
const isNew = !moduleOriginal || !moduleOriginal.b_code;
|
||||
if (moduleData && JSON.stringify(moduleData) !== JSON.stringify(moduleOriginal)) {
|
||||
saveReqs.push({
|
||||
const moduleRequest = {
|
||||
table: "s_module",
|
||||
key_field: "b_id",
|
||||
[isNew ? "inserts" : "updates"]: [moduleData],
|
||||
});
|
||||
};
|
||||
if (isNew) {
|
||||
moduleRequest.inserts = [moduleData];
|
||||
} else {
|
||||
moduleRequest.updates = [moduleData];
|
||||
}
|
||||
saveReqs.push(moduleRequest);
|
||||
}
|
||||
|
||||
// 子表:fields, listConfig, editConfig, queryConfig, powers
|
||||
@@ -558,14 +570,13 @@ async function saveAll() {
|
||||
}
|
||||
|
||||
// 多语言:由各子面板收集变更,统一在此保存
|
||||
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
|
||||
return;
|
||||
if (!selectedModule.value || String(selectedModule.value.b_id) !== moduleId) return;
|
||||
const i18nPanel = i18nPanelRef.value;
|
||||
if (i18nPanel && typeof i18nPanel.reconcileManagedRows === "function") {
|
||||
// 保存时才按最终草稿态生成受管键(模块编码/字段/权限),避免编辑过程中实时生成。
|
||||
i18nPanel.reconcileManagedRows(finalI18nItems.value);
|
||||
}
|
||||
if (typeof i18nPanel?.buildSaveRequest === "function") {
|
||||
if (i18nPanel && typeof i18nPanel.buildSaveRequest === "function") {
|
||||
const i18nReq = await i18nPanel.buildSaveRequest();
|
||||
if (i18nReq) saveReqs.push(i18nReq);
|
||||
}
|
||||
@@ -579,8 +590,11 @@ async function saveAll() {
|
||||
|
||||
// 更新 originals
|
||||
originals.value = cloneData(drafts.value);
|
||||
i18nPanelRef.value?.markSaved?.();
|
||||
if (i18nPanelRef.value) {
|
||||
i18nPanelRef.value.markSaved();
|
||||
}
|
||||
updateSavedTreeNode(moduleData);
|
||||
if (appStore.locale) await appStore.loadTranslations(appStore.locale);
|
||||
message.success("保存成功");
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e.message || e));
|
||||
@@ -591,12 +605,16 @@ async function saveAll() {
|
||||
|
||||
// ---- 字段同步与默认配置 ----
|
||||
function configuredFieldIds(section) {
|
||||
return new Set(
|
||||
[...(originals.value[section] || []), ...(drafts.value[section] || [])]
|
||||
.map((row) => row.b_field_id)
|
||||
.filter((id) => id != null)
|
||||
.map(String),
|
||||
);
|
||||
const ids = new Set();
|
||||
const originalRows = originals.value[section] || [];
|
||||
const draftRows = drafts.value[section] || [];
|
||||
for (const row of originalRows) {
|
||||
if (row.b_field_id != null) ids.add(String(row.b_field_id));
|
||||
}
|
||||
for (const row of draftRows) {
|
||||
if (row.b_field_id != null) ids.add(String(row.b_field_id));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function buildMissingDefaultConfigs(module, fields) {
|
||||
@@ -606,77 +624,84 @@ function buildMissingDefaultConfigs(module, fields) {
|
||||
const queryFieldIds = configuredFieldIds("queryConfig");
|
||||
const generated = { listConfig: [], editConfig: [], queryConfig: [] };
|
||||
|
||||
fields
|
||||
.filter((field) => field.b_canuse === 1)
|
||||
.forEach((field, index) => {
|
||||
const fieldId = String(field.b_id);
|
||||
const order = (index + 1) * 10;
|
||||
let orderIndex = 0;
|
||||
for (const field of fields) {
|
||||
if (field.b_canuse !== 1) continue;
|
||||
orderIndex++;
|
||||
const fieldId = String(field.b_id);
|
||||
const order = orderIndex * 10;
|
||||
|
||||
if (!listFieldIds.has(fieldId)) {
|
||||
generated.listConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_group_id: null,
|
||||
b_title: null,
|
||||
b_i18n: field.b_i18n || null,
|
||||
b_visible: 1,
|
||||
b_xh: order,
|
||||
b_width: 100,
|
||||
b_sortable: 0,
|
||||
});
|
||||
}
|
||||
if (!listFieldIds.has(fieldId)) {
|
||||
generated.listConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_group_id: null,
|
||||
b_title: null,
|
||||
b_i18n: field.b_i18n || null,
|
||||
b_visible: 1,
|
||||
b_xh: order,
|
||||
b_width: 100,
|
||||
b_sortable: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (!editFieldIds.has(fieldId)) {
|
||||
generated.editConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_group_id: null,
|
||||
b_xh: order,
|
||||
});
|
||||
}
|
||||
if (!editFieldIds.has(fieldId)) {
|
||||
generated.editConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_required: 0,
|
||||
b_readonly: 0,
|
||||
b_disabled: 0,
|
||||
b_group_id: null,
|
||||
b_xh: order,
|
||||
});
|
||||
}
|
||||
|
||||
if (!queryFieldIds.has(fieldId)) {
|
||||
generated.queryConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_component: "input",
|
||||
b_operator: "=",
|
||||
b_default_value: null,
|
||||
b_xh: order,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!queryFieldIds.has(fieldId)) {
|
||||
generated.queryConfig.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: moduleId,
|
||||
b_field_id: fieldId,
|
||||
b_component: "input",
|
||||
b_operator: "=",
|
||||
b_default_value: null,
|
||||
b_xh: order,
|
||||
b_canuse: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return generated;
|
||||
}
|
||||
|
||||
async function syncFields() {
|
||||
const module = selectedModule.value;
|
||||
const view = String(module?.b_viewtable || "").trim();
|
||||
if (!module) return;
|
||||
const view = String(module.b_viewtable || "").trim();
|
||||
if (!view || busy.value) return;
|
||||
const moduleId = String(module.b_id);
|
||||
const currentOperationId = ++operationId;
|
||||
|
||||
try {
|
||||
configSyncing.value = true;
|
||||
const res = await describeApi(view);
|
||||
if (currentOperationId !== operationId || String(selectedModule.value?.b_id) !== moduleId)
|
||||
return;
|
||||
if (!selectedModule.value || String(selectedModule.value.b_id) !== moduleId) return;
|
||||
const columns = res.data || [];
|
||||
const existingFields = drafts.value.fields || [];
|
||||
|
||||
// 构建视图列名集合
|
||||
const viewColumnSet = new Set(columns.map((c) => c.name.toLowerCase()));
|
||||
const viewColumnSet = new Set();
|
||||
for (const column of columns) {
|
||||
viewColumnSet.add(column.name.toLowerCase());
|
||||
}
|
||||
|
||||
// 已有字段映射(按字段名)
|
||||
const existingMap = new Map(existingFields.map((f) => [f.b_field?.toLowerCase(), f]));
|
||||
const existingMap = new Map();
|
||||
for (const field of existingFields) {
|
||||
if (!field.b_field) continue;
|
||||
existingMap.set(field.b_field.toLowerCase(), field);
|
||||
}
|
||||
|
||||
const mergedFields = [];
|
||||
|
||||
@@ -704,15 +729,16 @@ async function syncFields() {
|
||||
|
||||
// 视图中不存在的已有字段标记为不可用
|
||||
for (const existing of existingFields) {
|
||||
if (!viewColumnSet.has(existing.b_field?.toLowerCase())) {
|
||||
const fieldName = existing.b_field ? existing.b_field.toLowerCase() : "";
|
||||
if (!viewColumnSet.has(fieldName)) {
|
||||
mergedFields.push({ ...existing, b_canuse: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// 多语言键始终按 field.<模块代码>.<字段编码> 自动生成。
|
||||
mergedFields.forEach((field) => {
|
||||
for (const field of mergedFields) {
|
||||
field.b_i18n = buildModuleItemI18nKey("field", module.b_code, field.b_field);
|
||||
});
|
||||
}
|
||||
|
||||
// 先为新字段分配真实 ID,后续配置通过外键引用这些 ID。
|
||||
const changes = diffRows(mergedFields, existingFields);
|
||||
@@ -725,38 +751,48 @@ async function syncFields() {
|
||||
|
||||
// 为"有 b_i18n 键"的字段生成多语言默认配置(b_i18n 草稿行),注入 i18n 面板,
|
||||
// 由顶部"保存"统一落库;已存在的键由面板去重,不覆盖用户配置。
|
||||
const candidateI18n = mergedFields
|
||||
.filter((f) => f.b_canuse === 1 && f.b_i18n)
|
||||
.map((f) => ({
|
||||
key: f.b_i18n,
|
||||
value: f.b_name || f.b_field,
|
||||
managedId: `field:${f.b_id}`,
|
||||
}));
|
||||
let primaryLocale = "zh-CN";
|
||||
const candidateI18n = [];
|
||||
for (const field of mergedFields) {
|
||||
if (field.b_canuse !== 1 || !field.b_i18n) continue;
|
||||
candidateI18n.push({
|
||||
key: field.b_i18n,
|
||||
value: field.b_name || field.b_field,
|
||||
managedId: `field:${field.b_id}`,
|
||||
});
|
||||
}
|
||||
let primaryLocale = "";
|
||||
try {
|
||||
const langRes = await loadDataApi("b_i18n_type", "b_canuse = 1", "b_xh ASC, b_id ASC");
|
||||
const langRes = await loadDataApi(
|
||||
"b_i18n_type",
|
||||
"b_canuse = 1",
|
||||
"b_default DESC, b_xh ASC, b_id ASC",
|
||||
);
|
||||
const langs = langRes.data || [];
|
||||
if (langs.length) {
|
||||
// 默认字段名翻译写到"简体中文"(zh-CN),找不到再退回排序第一的语言
|
||||
const zh = langs.find((l) => l.b_code === "zh-CN");
|
||||
primaryLocale = (zh || langs[0]).b_code;
|
||||
const defaultLanguage = langs.find((item) => Number(item.b_default) === 1);
|
||||
primaryLocale = String(defaultLanguage?.b_code || langs[0].b_code || "");
|
||||
}
|
||||
} catch {
|
||||
primaryLocale = "";
|
||||
}
|
||||
const i18nEntries = [];
|
||||
if (primaryLocale) {
|
||||
for (const item of candidateI18n) {
|
||||
i18nEntries.push({
|
||||
key: item.key,
|
||||
locale: primaryLocale,
|
||||
value: item.value,
|
||||
managedId: item.managedId,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 取不到语言类型时回退默认值
|
||||
}
|
||||
const i18nEntries = candidateI18n.map((e) => ({
|
||||
key: e.key,
|
||||
locale: primaryLocale,
|
||||
value: e.value,
|
||||
managedId: e.managedId,
|
||||
}));
|
||||
|
||||
const generated = buildMissingDefaultConfigs(module, mergedFields);
|
||||
const generatedSections = ["listConfig", "editConfig", "queryConfig"];
|
||||
const generatedCount = generatedSections.reduce(
|
||||
(total, section) => total + generated[section].length,
|
||||
0,
|
||||
);
|
||||
let generatedCount = 0;
|
||||
for (const section of generatedSections) {
|
||||
generatedCount += generated[section].length;
|
||||
}
|
||||
|
||||
if (!hasChanges(fieldReq) && !generatedCount && !i18nEntries.length) {
|
||||
message.success("同步成功");
|
||||
@@ -765,8 +801,8 @@ async function syncFields() {
|
||||
|
||||
// 多语言默认配置注入 i18n 面板草稿;字段已预分配真实 ID(见上方 allocateTemporaryIds),
|
||||
// 列表/编辑/查询配置稍后追加到草稿。全部由顶部"保存"统一提交,期间不写库。
|
||||
if (i18nEntries.length) {
|
||||
i18nPanelRef.value?.mergeDefaults?.(i18nEntries);
|
||||
if (i18nEntries.length && i18nPanelRef.value) {
|
||||
i18nPanelRef.value.mergeDefaults(i18nEntries);
|
||||
}
|
||||
|
||||
for (const section of generatedSections) {
|
||||
@@ -850,14 +886,14 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
<div class="mm-header-center">
|
||||
<a-space size="middle">
|
||||
<span>启用</span>
|
||||
<a-switch
|
||||
<a-checkbox
|
||||
:checked="drafts.module?.b_canuse === 1"
|
||||
:disabled="busy"
|
||||
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
<a-divider type="vertical" />
|
||||
<span>显示菜单</span>
|
||||
<a-switch
|
||||
<a-checkbox
|
||||
:checked="drafts.module?.b_canmenu === 1"
|
||||
:disabled="busy || drafts.module?.b_module_type === 'data'"
|
||||
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
|
||||
@@ -891,6 +927,7 @@ void loadTree().catch((e) => message.error("模块树加载失败:" + (e.messa
|
||||
:fields="drafts.fields"
|
||||
:loading="busy"
|
||||
@update="(v) => onUpdate('fields', v)"
|
||||
@i18n-saved="onI18nSaved"
|
||||
/>
|
||||
<ModuleListConfigPanel
|
||||
v-else-if="tab.key === 'list'"
|
||||
|
||||
@@ -1,163 +1,143 @@
|
||||
<script setup>
|
||||
import { computed, h, onMounted, ref, shallowRef } from "vue";
|
||||
import { message, Switch, Button, Popconfirm, Tag } from "antdv-next";
|
||||
import { Plus } from "@lucide/vue";
|
||||
import FmsTable from "@/components/fms-table/FmsTable.vue";
|
||||
import { loadDataApi, saveObjectApi, nextIdApi } from "@/services/api";
|
||||
import { usePermissionStore } from "@/stores/permissions";
|
||||
import { computed, ref } from "vue";
|
||||
import { message, Modal } from "antdv-next";
|
||||
import { Plus, Trash2 } from "@lucide/vue";
|
||||
import FmsModuleListPage from "@/components/fms-module-list/FmsModuleListPage.vue";
|
||||
import { loadDataApi, nextIdApi, saveObjectApi } from "@/services/api";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
|
||||
const data = ref([]);
|
||||
const loading = ref(false);
|
||||
const appStore = useAppStore();
|
||||
const listRef = ref(null);
|
||||
const selectedRowKeys = ref([]);
|
||||
const saving = ref(false);
|
||||
|
||||
const canCreate = computed(() => permissionStore.canPower("m_i18n_type", "create"));
|
||||
const canUpdate = computed(() => permissionStore.canPower("m_i18n_type", "update"));
|
||||
const canDelete = computed(() => permissionStore.canPower("m_i18n_type", "delete"));
|
||||
const rowSelection = computed(() => {
|
||||
return {
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
getCheckboxProps: (row) => ({ disabled: Number(row.b_default) === 1 }),
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
const rowActions = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: "edit",
|
||||
label: appStore.t("common.edit", "编辑"),
|
||||
disabled: () => saving.value,
|
||||
onClick: openEdit,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
type: "delete",
|
||||
label: appStore.t("common.delete", "删除"),
|
||||
confirm: appStore.t("common.confirmDeleteLanguage", "确定删除该语言?"),
|
||||
disabled: ({ record }) => saving.value || Number(record.b_default) === 1,
|
||||
onClick: removeRow,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
async function reloadList() {
|
||||
selectedRowKeys.value = [];
|
||||
if (listRef.value) await listRef.value.reload();
|
||||
try {
|
||||
const res = await loadDataApi("b_i18n_type", "", "b_xh ASC, b_id ASC");
|
||||
data.value = res.data || [];
|
||||
} catch (e) {
|
||||
message.error("加载多语言类型失败:" + (e?.message || e));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
await appStore.loadLanguages();
|
||||
} catch (error) {
|
||||
message.error("刷新语言配置失败:" + (error?.message || error));
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData);
|
||||
|
||||
/* ---------- 行内单元格渲染 ---------- */
|
||||
function createDefaultCell() {
|
||||
return (props) =>
|
||||
props.row.b_default === 1
|
||||
? h(Tag, { color: "blue" }, "默认")
|
||||
: h("span", { class: "muted" }, "—");
|
||||
}
|
||||
|
||||
function createCanuseCell() {
|
||||
return (props) => {
|
||||
const row = props.row;
|
||||
return h(Switch, {
|
||||
checked: row.b_canuse === 1,
|
||||
size: "small",
|
||||
disabled: !canUpdate.value,
|
||||
"onUpdate:checked": (val) => {
|
||||
const next = val ? 1 : 0;
|
||||
if (row.b_canuse === next) return;
|
||||
row.b_canuse = next;
|
||||
saveRow(row, { b_canuse: next });
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createActionsCell() {
|
||||
return (props) => {
|
||||
const row = props.row;
|
||||
const isDefault = row.b_default === 1;
|
||||
const children = [];
|
||||
if (canUpdate.value) {
|
||||
children.push(
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
type: "link",
|
||||
size: "small",
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
openEdit(row);
|
||||
},
|
||||
},
|
||||
"编辑",
|
||||
),
|
||||
);
|
||||
}
|
||||
if (canDelete.value && !isDefault) {
|
||||
children.push(
|
||||
h(
|
||||
Popconfirm,
|
||||
{
|
||||
title: "确定删除该语言?",
|
||||
okText: "删除",
|
||||
okButtonProps: { danger: true },
|
||||
onConfirm: () => removeRow(row),
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{ type: "link", size: "small", danger: true, onClick: (e) => e.stopPropagation() },
|
||||
"删除",
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return h("span", { class: "actions-cell" }, children);
|
||||
};
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
{ dataIndex: "b_code", title: "简称 / 编码", width: 150 },
|
||||
{ dataIndex: "b_name", title: "名称", width: 160 },
|
||||
{
|
||||
dataIndex: "b_default",
|
||||
title: "默认",
|
||||
width: 80,
|
||||
align: "center",
|
||||
customCell: createDefaultCell(),
|
||||
},
|
||||
{
|
||||
dataIndex: "b_canuse",
|
||||
title: "是否启用",
|
||||
width: 100,
|
||||
align: "center",
|
||||
customCell: createCanuseCell(),
|
||||
},
|
||||
{ dataIndex: "b_xh", title: "排序", width: 80, align: "center" },
|
||||
{
|
||||
dataIndex: "actions",
|
||||
title: "操作",
|
||||
width: 130,
|
||||
align: "center",
|
||||
customCell: createActionsCell(),
|
||||
},
|
||||
]);
|
||||
|
||||
/* ---------- 行内开关即时保存 ---------- */
|
||||
async function saveRow(row, patch) {
|
||||
try {
|
||||
await saveObjectApi([
|
||||
{ table: "b_i18n_type", key_field: "b_id", updates: [{ b_id: row.b_id, ...patch }] },
|
||||
]);
|
||||
message.success("已更新");
|
||||
} catch (e) {
|
||||
message.error("更新失败:" + (e?.message || e));
|
||||
await loadData();
|
||||
async function getOtherDefaultUpdates(currentId) {
|
||||
const response = await loadDataApi("b_i18n_type", "b_default = 1", "b_id ASC");
|
||||
const updates = [];
|
||||
for (const row of response.data || []) {
|
||||
if (String(row.b_id) === String(currentId)) continue;
|
||||
updates.push({ b_id: row.b_id, b_default: 0 });
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
async function removeRow(row) {
|
||||
if (row.b_default === 1) return;
|
||||
async function updateCheckbox(row, field, checked) {
|
||||
if (saving.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const value = checked ? 1 : 0;
|
||||
const updates = [{ b_id: row.b_id, [field]: value }];
|
||||
if (field === "b_default" && value === 1) {
|
||||
updates.push(...(await getOtherDefaultUpdates(row.b_id)));
|
||||
}
|
||||
await saveObjectApi([
|
||||
{ table: "b_i18n_type", key_field: "b_id", deletes: [{ b_id: row.b_id }] },
|
||||
{ table: "b_i18n_type", key_field: "b_id", inserts: [], updates, deletes: [] },
|
||||
]);
|
||||
message.success("删除成功");
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
message.error("删除失败:" + (e?.message || e));
|
||||
message.success(appStore.t("common.updateSuccess", "更新成功"));
|
||||
await reloadList();
|
||||
} catch (error) {
|
||||
message.error(appStore.t("common.updateFailed", "更新失败") + ":" + (error?.message || error));
|
||||
await reloadList();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 新增 / 编辑弹窗 ---------- */
|
||||
async function removeRow(row) {
|
||||
if (saving.value || Number(row.b_default) === 1) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_i18n_type",
|
||||
key_field: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: [{ b_id: row.b_id }],
|
||||
},
|
||||
]);
|
||||
message.success(appStore.t("common.deleteSuccess", "删除成功"));
|
||||
await reloadList();
|
||||
} catch (error) {
|
||||
message.error(appStore.t("common.deleteFailed", "删除失败") + ":" + (error?.message || error));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function batchDelete() {
|
||||
if (saving.value || !selectedRowKeys.value.length) return;
|
||||
const keys = [...selectedRowKeys.value];
|
||||
Modal.confirm({
|
||||
title: `${appStore.t("common.confirmBatchDelete", "确定删除选中的记录?")} (${keys.length})`,
|
||||
okText: appStore.t("common.delete", "删除"),
|
||||
okType: "danger",
|
||||
cancelText: appStore.t("common.cancel", "取消"),
|
||||
onOk: async () => {
|
||||
saving.value = true;
|
||||
try {
|
||||
const deletes = keys.map((b_id) => ({ b_id }));
|
||||
await saveObjectApi([
|
||||
{
|
||||
table: "b_i18n_type",
|
||||
key_field: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes,
|
||||
},
|
||||
]);
|
||||
message.success(appStore.t("common.deleteSuccess", "删除成功"));
|
||||
await reloadList();
|
||||
} catch (error) {
|
||||
message.error(
|
||||
appStore.t("common.deleteFailed", "删除失败") + ":" + (error?.message || error),
|
||||
);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const modalVisible = ref(false);
|
||||
const modalMode = ref("create");
|
||||
const editingId = ref(null);
|
||||
@@ -174,63 +154,69 @@ function openCreate() {
|
||||
modalMode.value = "create";
|
||||
editingId.value = null;
|
||||
editingDefault.value = false;
|
||||
form.value = { b_code: "", b_name: "", b_xh: 0, defaultOn: false, canuseOn: true };
|
||||
form.value = {
|
||||
b_code: "",
|
||||
b_name: "",
|
||||
b_xh: 0,
|
||||
defaultOn: false,
|
||||
canuseOn: true,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
modalMode.value = "edit";
|
||||
editingId.value = row.b_id;
|
||||
editingDefault.value = row.b_default === 1;
|
||||
editingDefault.value = Number(row.b_default) === 1;
|
||||
form.value = {
|
||||
b_code: row.b_code,
|
||||
b_name: row.b_name,
|
||||
b_code: row.b_code || "",
|
||||
b_name: row.b_name || "",
|
||||
b_xh: row.b_xh ?? 0,
|
||||
defaultOn: row.b_default === 1,
|
||||
canuseOn: row.b_canuse === 1,
|
||||
defaultOn: Number(row.b_default) === 1,
|
||||
canuseOn: Number(row.b_canuse) === 1,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
async function saveForm() {
|
||||
const f = form.value;
|
||||
if (!f.b_code || !f.b_code.trim()) {
|
||||
message.warning("请填写语言编码");
|
||||
const current = form.value;
|
||||
if (!String(current.b_code || "").trim()) {
|
||||
message.warning(appStore.t("common.enterLanguageCode", "请填写语言编码"));
|
||||
return;
|
||||
}
|
||||
if (!f.b_name || !f.b_name.trim()) {
|
||||
message.warning("请填写语言名称");
|
||||
if (!String(current.b_name || "").trim()) {
|
||||
message.warning(appStore.t("common.enterLanguageName", "请填写语言名称"));
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const isNew = modalMode.value === "create" || !editingId.value;
|
||||
const b_id = isNew ? (await nextIdApi(1)).data : editingId.value;
|
||||
const isNew = modalMode.value === "create";
|
||||
const b_id = isNew ? (await nextIdApi()).data : editingId.value;
|
||||
const row = {
|
||||
b_id,
|
||||
b_code: f.b_code.trim(),
|
||||
b_name: f.b_name.trim(),
|
||||
b_canuse: f.canuseOn ? 1 : 0,
|
||||
b_default: f.defaultOn ? 1 : 0,
|
||||
b_xh: f.b_xh ?? 0,
|
||||
b_code: current.b_code.trim(),
|
||||
b_name: current.b_name.trim(),
|
||||
b_default: current.defaultOn ? 1 : 0,
|
||||
b_canuse: current.canuseOn ? 1 : 0,
|
||||
b_xh: current.b_xh ?? 0,
|
||||
};
|
||||
const request = {
|
||||
table: "b_i18n_type",
|
||||
key_field: "b_id",
|
||||
inserts: isNew ? [row] : [],
|
||||
updates: isNew ? [] : [row],
|
||||
deletes: [],
|
||||
};
|
||||
const req = { table: "b_i18n_type", key_field: "b_id", inserts: [], updates: [], deletes: [] };
|
||||
if (isNew) req.inserts.push(row);
|
||||
else req.updates.push(row);
|
||||
// 保证仅一个默认语言:若本次设为默认,则将其它语言取消默认
|
||||
if (row.b_default === 1) {
|
||||
for (const r of data.value) {
|
||||
if (r.b_id !== b_id && r.b_default === 1) {
|
||||
req.updates.push({ b_id: r.b_id, b_default: 0 });
|
||||
}
|
||||
}
|
||||
request.updates.push(...(await getOtherDefaultUpdates(b_id)));
|
||||
}
|
||||
await saveObjectApi([req]);
|
||||
message.success("保存成功");
|
||||
await saveObjectApi([request]);
|
||||
message.success(appStore.t("common.saveSuccess", "保存成功"));
|
||||
modalVisible.value = false;
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
message.error("保存失败:" + (e?.message || e));
|
||||
await reloadList();
|
||||
} catch (error) {
|
||||
message.error(appStore.t("common.saveFailed", "保存失败") + ":" + (error?.message || error));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@@ -239,106 +225,90 @@ async function saveForm() {
|
||||
|
||||
<template>
|
||||
<div class="i18n-type-page">
|
||||
<div class="panel-card">
|
||||
<div class="panel-toolbar">
|
||||
<a-button v-if="canCreate" type="primary" @click="openCreate">
|
||||
<Plus :size="16" /> 新增语言
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FmsTable
|
||||
v-model:columns="columns"
|
||||
:data-source="data"
|
||||
row-key="b_id"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
<FmsModuleListPage
|
||||
ref="listRef"
|
||||
:row-actions="rowActions"
|
||||
:row-selection="rowSelection"
|
||||
@loaded="selectedRowKeys = []"
|
||||
>
|
||||
<template #actions="{ loading }">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="loading || saving"
|
||||
@click="openCreate"
|
||||
>
|
||||
<Plus :size="16" />
|
||||
{{ appStore.t("common.create", "新增") }}
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
:disabled="loading || saving || !selectedRowKeys.length"
|
||||
@click="batchDelete"
|
||||
>
|
||||
<Trash2 :size="16" />
|
||||
{{ appStore.t("common.batchDelete", "批量删除")
|
||||
}}{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : "" }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<template #cell-b_canuse="{ value, record }">
|
||||
<a-checkbox
|
||||
:checked="Number(value) === 1"
|
||||
:disabled="saving"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
@update:checked="(checked) => updateCheckbox(record, 'b_canuse', checked)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-b_default="{ value, record }">
|
||||
<a-checkbox
|
||||
:checked="Number(value) === 1"
|
||||
:disabled="saving"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
@update:checked="(checked) => updateCheckbox(record, 'b_default', checked)"
|
||||
/>
|
||||
</template>
|
||||
</FmsModuleListPage>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalMode === 'create' ? '新增语言' : '编辑语言'"
|
||||
:title="
|
||||
modalMode === 'create'
|
||||
? appStore.t('common.createLanguage', '新增语言')
|
||||
: appStore.t('common.editLanguage', '编辑语言')
|
||||
"
|
||||
:confirm-loading="saving"
|
||||
@ok="saveForm"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="语言编码(简称)" required>
|
||||
<a-form-item :label="appStore.t('common.languageCode', '语言编码')" required>
|
||||
<a-input v-model:value="form.b_code" :disabled="editingDefault" />
|
||||
</a-form-item>
|
||||
<a-form-item label="语言名称" required>
|
||||
<a-form-item :label="appStore.t('common.languageName', '语言名称')" required>
|
||||
<a-input v-model:value="form.b_name" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否默认">
|
||||
<a-switch v-model:checked="form.defaultOn" :disabled="editingDefault" />
|
||||
<span class="form-tip">设为默认后,其它语言将自动取消默认</span>
|
||||
<a-form-item :label="appStore.t('common.defaultLanguage', '默认语言')">
|
||||
<a-checkbox v-model:checked="form.defaultOn" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否启用">
|
||||
<a-switch v-model:checked="form.canuseOn" />
|
||||
<a-form-item :label="appStore.t('common.enabled', '启用')">
|
||||
<a-checkbox v-model:checked="form.canuseOn" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序">
|
||||
<a-input-number v-model:value="form.b_xh" :min="0" style="width: 100%" />
|
||||
<a-form-item :label="appStore.t('common.sort', '排序')">
|
||||
<a-input-number v-model:value="form.b_xh" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.i18n-type-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
}
|
||||
</style>
|
||||
+7
-10
@@ -57,11 +57,11 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
约束:
|
||||
|
||||
- `b_parent_id` 外键引用 `s_module.b_id`;保存服务校验父节点不形成循环。
|
||||
- `b_code` 唯一,格式为 `^[a-z][a-z0-9_]{1,49}$`;`b_route` 非空时唯一。
|
||||
- `b_code` 唯一;`b_route` 非空时唯一。
|
||||
- `directory` 不能配置 `b_route`、`b_viewtable` 或 `b_savetable`。
|
||||
- `page` 必须配置 `b_route`;其业务数据由页面组合的数据模块提供。
|
||||
- `data` 不能显示菜单且不能配置 `b_route`;必须配置 `b_viewtable`,保存场景再按需配置 `b_savetable` 与 `b_keyfield`。
|
||||
- `b_canmenu`、`b_canuse` 只能为 `0` 或 `1`。建议建立 `(b_parent_id, b_xh, b_id)` 索引。
|
||||
- 建议建立 `(b_parent_id, b_xh, b_id)` 索引。
|
||||
|
||||
## 字段来源与同步
|
||||
|
||||
@@ -76,7 +76,7 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
| `b_field` | `varchar(50)` | 视图字段名 |
|
||||
| `b_name` | `nvarchar(100)` | 默认字段名称,由管理员维护 |
|
||||
| `b_i18n` | `varchar(150)` | 多语言键 |
|
||||
| `b_type` | `varchar(30)` | 字段控件类型:`input`、`textarea`、`number`、`money`、`date`、`datetime`、`switch` 或 `select` |
|
||||
| `b_type` | `varchar(30)` | 字段控件类型:`input`、`textarea`、`number`、`money`、`date`、`datetime`、`checkbox` 或 `select` |
|
||||
| `b_default_value` | `nvarchar(500)` | 新增记录时的默认值 |
|
||||
| `b_options` | `nvarchar(max)` | 类型专属 JSON 配置,例如选择器来源、日期格式或金额精度 |
|
||||
| `b_canuse` | `int` | 是否启用 |
|
||||
@@ -84,7 +84,7 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
约束:
|
||||
|
||||
- `b_module_id` 外键引用 `s_module.b_id`,`(b_module_id, b_field)` 唯一。
|
||||
- `b_canuse` 只能为 `0` 或 `1`;`b_options` 非空时通过 `ISJSON` 校验。建议建立 `(b_module_id, b_canuse, b_id)` 索引。
|
||||
- 建议建立 `(b_module_id, b_canuse, b_id)` 索引。
|
||||
- 字段没有 list、edit 或 query 配置记录时,不参与对应场景。
|
||||
- `b_type` 是展示和编辑的唯一类型来源:Table 按它渲染默认显示器,Form 或可编辑 Table 按它选择控件。
|
||||
- `select` 是否允许自由输入、选项来源、值字段和显示字段均由 `b_options` 控制。
|
||||
@@ -95,7 +95,7 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
|
||||
1. 后端校验视图名并读取列名、SQL 类型和可空性等结构信息。
|
||||
2. 按列名查找同模块的 `s_module_field`;新列创建字段基础记录。
|
||||
3. 新字段按 SQL 类型初始化 `b_type`,例如字符串映射为 `input`、长文本映射为 `textarea`、数值映射为 `number`、日期映射为 `date` 或 `datetime`、位值映射为 `switch`。已有字段不覆盖管理员维护的 `b_type` 与 `b_options`;`select`、`money` 等业务类型由管理员按语义调整。
|
||||
3. 新字段按 SQL 类型初始化 `b_type`,例如字符串映射为 `input`、长文本映射为 `textarea`、数值映射为 `number`、日期映射为 `date` 或 `datetime`、位值映射为 `checkbox`。已有字段不覆盖管理员维护的 `b_type` 与 `b_options`;`select`、`money` 等业务类型由管理员按语义调整。
|
||||
4. 本次视图中不存在的已有字段保留其记录和全部场景配置,但将 `b_canuse` 置为 `0`。
|
||||
5. 不覆盖人工维护的 `b_name`、`b_i18n`、`b_default_value` 与 `b_options`,不物理删除字段或场景配置。
|
||||
|
||||
@@ -120,7 +120,6 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
|
||||
- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`,`b_group_id` 引用同模块的 `s_module_field_group`。
|
||||
- `b_field_id` 必填;同一字段在同一模块的列表中最多出现一次,使用 `(b_module_id, b_field_id)` 唯一约束。
|
||||
- `b_visible` 只能为 `0` 或 `1`。
|
||||
- 字段列的显示器由关联字段的 `b_type` 与 `b_options` 决定,例如 `money`、`date`、`datetime` 均由前端按统一规则渲染。建议建立 `(b_module_id, b_group_id, b_xh, b_id)` 索引。
|
||||
- 不保存 `level`、`colspan`、`rowspan`;前端加载 Table 分组树和字段列后,按分组的 `b_parent_id`、`b_xh` 组装嵌套表头。隐藏分组的后代不显示;没有可见字段后代的分组不渲染。
|
||||
|
||||
@@ -152,7 +151,6 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
约束:
|
||||
|
||||
- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`;`(b_module_id, b_field_id)` 唯一,保存服务校验字段属于同一模块。
|
||||
- `b_required`、`b_readonly`、`b_disabled` 只能为 `0` 或 `1`。
|
||||
- `b_group_id` 必须关联同模块的 `s_module_field_group`;分组为空时字段进入默认编辑区域。
|
||||
- 已分组字段按分组的 `b_xh` 排列,组内按 `b_xh` 排列;默认区域字段按 `b_xh` 排列。
|
||||
|
||||
@@ -179,7 +177,7 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
- `b_module_id` 外键引用 `s_module.b_id`;`b_id` 是分组的稳定关联键。
|
||||
- `b_parent_id` 必须引用同模块的分组且不能形成循环。
|
||||
- 多级分组可用于 FmsTable 表头;FmsForm 与 FmsTable 如何消费编辑分组由各自实现决定。
|
||||
- `b_canuse` 只能为 `0` 或 `1`。建议建立 `(b_module_id, b_canuse, b_xh, b_id)` 索引。
|
||||
- 建议建立 `(b_module_id, b_canuse, b_xh, b_id)` 索引。
|
||||
- 无字段引用的分组不渲染;分组停用时,其关联字段或列不显示。
|
||||
|
||||
视图型选择器的 `b_options` 示例:
|
||||
@@ -216,7 +214,6 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
约束:
|
||||
|
||||
- `b_module_id` 外键引用 `s_module.b_id`,`b_field_id` 外键引用 `s_module_field.b_id`;`(b_module_id, b_field_id)` 唯一,保存服务校验模块一致性。
|
||||
- `b_canuse` 只能为 `0` 或 `1`;`b_operator` 必须属于定义的操作符集合。
|
||||
- 查询组件与操作符由后端白名单校验;查询值必须参数化,不能由配置直接拼接 SQL。
|
||||
- 建议建立 `(b_module_id, b_canuse, b_xh, b_id)` 索引。
|
||||
|
||||
@@ -236,7 +233,7 @@ s_module / s_module_field / s_module_field_list / s_module_field_group / s_modul
|
||||
| `b_xh` | `int` | 显示顺序 |
|
||||
| `b_canuse` | `int` | 是否启用,默认 `1` |
|
||||
|
||||
约束:`b_module_id` 外键引用 `s_module.b_id`,`(b_module_id, b_code)` 唯一,`b_canuse` 只能为 `0` 或 `1`。
|
||||
约束:`b_module_id` 外键引用 `s_module.b_id`,`(b_module_id, b_code)` 唯一。
|
||||
|
||||
### `dbo.b_user_module`
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ fms-api/config/migrations/003_module_surrogate_key.sql
|
||||
|
||||
脚本包含:
|
||||
|
||||
- 主键、外键、唯一约束和布尔字段 `0/1` 检查约束。
|
||||
- 主键、外键和唯一约束。
|
||||
- 模块树、字段列表和个人权限所需索引。
|
||||
- `XACT_ABORT ON` 和显式事务。
|
||||
- `IF NOT EXISTS`,保证脚本可以重复执行。
|
||||
@@ -148,7 +148,7 @@ b_i18n
|
||||
- 同一模块字段名和按钮编码不重复。
|
||||
- 表名、视图名和字段名只允许合法数据库标识符。
|
||||
|
||||
数据库外键、唯一约束和检查约束负责最后兜底,任一表失败时由现有 `DataSaveService` 回滚整个请求。
|
||||
数据库外键和唯一约束负责最后兜底,任一表失败时由现有 `DataSaveService` 回滚整个请求。
|
||||
|
||||
### 模块删除
|
||||
|
||||
|
||||
Reference in new issue
Block a user