20260919211702
This commit is contained in:
1 parent
f56e70b050
commit
f4bfc1ddfc
97 files changed
+6059
-1712
No files matched your search
@@ -295,9 +295,12 @@ const resolvedWidth = computed(() => {
|
||||
/** 保存过程上下文:beforeSave 阶段准备的数据,buildSaveData / afterSave 阶段消费 */
|
||||
let saveCtx = { isNew: false, row: null, extraUpdates: [] }
|
||||
|
||||
/** 模块保存表(s_module.b_save_table 优先,其次 b_view_table) */
|
||||
/**
|
||||
* 模块保存表:只使用 s_module.b_save_table(设计 §6.1)。
|
||||
* 视图表通常不可写,回退到 b_view_table 会让写入在数据库层失败;为空时由 beforeSave 拒绝。
|
||||
*/
|
||||
function saveTableName() {
|
||||
return String(moduleMeta.value?.b_save_table || moduleMeta.value?.b_view_table || '')
|
||||
return String(moduleMeta.value?.b_save_table || '')
|
||||
}
|
||||
|
||||
/** 构造完整合并行(含主键;checkbox→0/1、''→null 归一化),供保存前准备与 diff 使用 */
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup>
|
||||
import { computed, h, nextTick, onActivated, reactive, ref, shallowRef, useSlots, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Columns3, Download, Ellipsis, FileSpreadsheet, RefreshCw, Save } from '@lucide/vue'
|
||||
import { Button, Dropdown, Message, Pagination, Spin } from '@/components/ui'
|
||||
import { loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from '@/services/api'
|
||||
import { Download, Ellipsis, FileSpreadsheet, RefreshCw, RotateCcw, Save } from '@lucide/vue'
|
||||
import { Button, ButtonGroup, Dropdown, Message, Pagination, Spin, Tag } from '@/components/ui'
|
||||
import { deleteApi, loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from '@/services/api'
|
||||
import { exportListApi, loadTotalsApi } from '@/services/exportService'
|
||||
import { useI18nStore } from '@/stores/i18n'
|
||||
import {
|
||||
@@ -33,16 +33,22 @@ import FmsAdvancedQueryPanel from './FmsAdvancedQueryPanel.vue'
|
||||
import FmsQuerySettingsDrawer from './FmsQuerySettingsDrawer.vue'
|
||||
import FmsExportDialog from './FmsExportDialog.vue'
|
||||
import {
|
||||
PREF_SCHEMA_TYPES,
|
||||
buildDefaultQueryLayout,
|
||||
columnSettingsDelta,
|
||||
currentUserId,
|
||||
isEmptyColumnDelta,
|
||||
isEmptyDelta,
|
||||
layoutDelta,
|
||||
loadFieldPower,
|
||||
loadModulePref,
|
||||
mergeColumnPref,
|
||||
mergeQueryLayout,
|
||||
removeModulePref,
|
||||
sameColumnSettings,
|
||||
saveModulePref,
|
||||
} from './useModulePref'
|
||||
import { usePermissionStore } from '@/stores/permissions'
|
||||
|
||||
const props = defineProps({
|
||||
dataCode: { type: String, default: '' },
|
||||
@@ -73,6 +79,7 @@ const emit = defineEmits([
|
||||
const slots = useSlots()
|
||||
const route = useRoute()
|
||||
const { t } = useI18nStore()
|
||||
const permissions = usePermissionStore()
|
||||
const tableRef = ref(null)
|
||||
// keep-alive 中的缓存实例必须保留各自的路由上下文,不能跟随全局 route 变化。
|
||||
const instanceRoutePath = route.path
|
||||
@@ -112,6 +119,17 @@ const queryLayout = ref({ area: {}, order: {} })
|
||||
const defaultQueryLayout = ref({ area: {}, order: {} })
|
||||
// query 界面配置 JSON(s_module_schema.query),「保存为通用」时回写
|
||||
const querySchema = shallowRef(null)
|
||||
// ── 栏位设置(列宽/顺序)──
|
||||
// 基线:进入本页(或保存/重载)时的栏位快照,用于判断「列是否已被调整」。
|
||||
// 与 tableRef.getColumnSettings() 的当前值逐项比对,决定工具条上「已调整」操作条是否出现。
|
||||
const columnBaseline = shallowRef([])
|
||||
const columnDirty = ref(false)
|
||||
// 个人层栏位偏好(s_user_module_pref, b_schema_type='view')是否已存在,决定 insert 还是 update
|
||||
const columnPrefRowExists = ref(false)
|
||||
// 通用栏位行(s_module_schema.view 展开结果):个人增量与其比较,「保存为通用」也以它为底
|
||||
const columnGlobalRows = shallowRef([])
|
||||
// 「保存为通用」会改到所有人的默认视图,限系统管理员(与 /module/* 的 meta.adminOnly 同一判据)
|
||||
const canSaveColumnGlobal = computed(() => permissions.isG3soft)
|
||||
// 无查询权限的字段(s_user_field_power.b_query = 0),设置抽屉中灰显
|
||||
const disabledFields = shallowRef([])
|
||||
const keyField = ref('')
|
||||
@@ -150,6 +168,7 @@ const effectiveRowDraggable = computed(
|
||||
)
|
||||
|
||||
// ── 外部工具 ──
|
||||
// 栏位设置不在此菜单:列宽/顺序改动后由工具条按需浮出操作条承载(见 columnDirty)
|
||||
const moreMenuItems = computed(() => [
|
||||
{
|
||||
key: 'batch-export',
|
||||
@@ -162,17 +181,6 @@ const moreMenuItems = computed(() => [
|
||||
label: t('common.refreshData', '刷新数据'),
|
||||
icon: h(RefreshCw, { size: 14 }),
|
||||
},
|
||||
{
|
||||
key: 'reset-columns',
|
||||
label: t('common.resetColumnSettings', '恢复栏位设置'),
|
||||
icon: h(Columns3, { size: 14 }),
|
||||
},
|
||||
{
|
||||
key: 'save-column-settings',
|
||||
label: t('common.saveColumnSettings', '保存栏位设置'),
|
||||
icon: h(Save, { size: 14 }),
|
||||
disabled: columnSaving.value,
|
||||
},
|
||||
])
|
||||
const cellSlotNames = computed(() => Object.keys(slots).filter((name) => name.startsWith('cell-')))
|
||||
// 高级面板可选字段:排除用户在设置中隐藏的字段
|
||||
@@ -311,6 +319,9 @@ async function loadModuleConfig() {
|
||||
if (!fieldById.has(String(item.b_field))) continue
|
||||
lists.push(item)
|
||||
}
|
||||
// 通用栏位(lists)即「保存为通用」的基线与个人增量的比较基准;
|
||||
// 个人层只叠加宽度与顺序,不增删字段,因此先裁到同一字段集再合并。
|
||||
columnGlobalRows.value = lists
|
||||
|
||||
// 查询条件:query JSON(quick 顺序优先)
|
||||
const queryRender = buildQueryRender(schemas.query)
|
||||
@@ -335,7 +346,11 @@ async function loadModuleConfig() {
|
||||
|
||||
// 两层配置(系统默认 → 个人)+ 字段查询权限;权限在偏好合并之后裁剪,不得放宽权限
|
||||
const userId = currentUserId()
|
||||
const savedPref = await loadModulePref(userId, dataId)
|
||||
// 查询布局(query)与栏位布局(view)各有一份个人层增量,互不影响
|
||||
const [savedPref, savedColumnPref] = await Promise.all([
|
||||
loadModulePref(userId, dataId),
|
||||
loadModulePref(userId, dataId, PREF_SCHEMA_TYPES.view),
|
||||
])
|
||||
const fieldPower = await loadFieldPower(userId, dataId)
|
||||
if (getDataCode() !== code) return false
|
||||
|
||||
@@ -358,6 +373,12 @@ async function loadModuleConfig() {
|
||||
defaultQueryLayout.value = buildDefaultQueryLayout(allowedQueries, queryRender.quick)
|
||||
queryLayout.value = mergeQueryLayout(defaultQueryLayout.value, savedPref, allowedFields)
|
||||
|
||||
// 栏位:通用(lists)→ 个人层叠加宽度与顺序;个人层不增删字段(通用没有的字段一律忽略)
|
||||
const mergedLists = mergeColumnPref(lists, savedColumnPref)
|
||||
columnPrefRowExists.value = Boolean(savedColumnPref)
|
||||
columnBaseline.value = columnRowsOf(mergedLists)
|
||||
columnDirty.value = false
|
||||
|
||||
// 默认条件取全部启用字段:简单查询与高级查询共用同一批启用字段
|
||||
const defaultValues = {}
|
||||
for (const item of allowedQueries) defaultValues[item.field.b_field] = defaultQueryValue(item)
|
||||
@@ -371,7 +392,7 @@ async function loadModuleConfig() {
|
||||
dataModule.value = runtimeData
|
||||
activeFields.value = fields
|
||||
activeFieldById.value = fieldById
|
||||
listConfig.value = lists
|
||||
listConfig.value = mergedLists
|
||||
queryItems.value = allowedQueries
|
||||
viewSchema.value = schemas.view
|
||||
editSchema.value = schemas.edit
|
||||
@@ -667,11 +688,96 @@ function reload() {
|
||||
return fetchData()
|
||||
}
|
||||
|
||||
/* ================= 栏位设置(列宽 / 顺序) =================
|
||||
* 编辑方式:直接在表格上拖拽列宽、拖拽列头换序。
|
||||
* 提交方式:工具条按需浮出「撤销 / 保存为个人 / 保存为通用」——
|
||||
* 拖完才出现,因此平时不占内容区,也不改变表格高度。
|
||||
* 默认落点是个人层(s_user_module_pref, b_schema_type='view',只存增量),
|
||||
* 「保存为通用」才写 s_module_schema(影响所有人,需要权限)。
|
||||
*/
|
||||
|
||||
/** 按当前 listConfig 重建列(丢弃未保存的拖拽结果);等 DOM 更新后再重置虚拟滚动 */
|
||||
async function resetColumns() {
|
||||
await nextTick()
|
||||
if (tableRef.value) tableRef.value.resetColumns()
|
||||
}
|
||||
|
||||
/** 栏位行 → 用于比对的精简结构(只含字段与宽度,顺序即数组顺序) */
|
||||
function columnRowsOf(rows) {
|
||||
return (rows || []).map((row) => ({ b_field: String(row.b_field), b_width: row.b_width }))
|
||||
}
|
||||
|
||||
/** 表格当前栏位设置(列宽取真实渲染宽度);表格未就绪时回退到已加载的 listConfig */
|
||||
function currentColumnSettings() {
|
||||
const fromTable = tableRef.value?.getColumnSettings?.()
|
||||
if (Array.isArray(fromTable) && fromTable.length) return columnRowsOf(fromTable)
|
||||
return columnRowsOf(listConfig.value)
|
||||
}
|
||||
|
||||
/** 重新比对当前栏位与基线,决定操作条是否出现 */
|
||||
function refreshColumnDirty() {
|
||||
columnDirty.value = !sameColumnSettings(currentColumnSettings(), columnBaseline.value)
|
||||
}
|
||||
|
||||
/** 表格列变化(拖宽 / 拖序)时由 FmsModuleTable 上抛 */
|
||||
function handleColumnsChange() {
|
||||
refreshColumnDirty()
|
||||
}
|
||||
|
||||
/** 撤销:丢弃未保存的列宽/顺序改动,回到基线 */
|
||||
async function undoColumnSettings() {
|
||||
await resetColumns()
|
||||
columnDirty.value = false
|
||||
}
|
||||
|
||||
/** 保存为个人:只记「相对通用的差异」,通用后续调整仍能透传到这个人 */
|
||||
async function saveColumnAsPersonal() {
|
||||
if (loading.value || columnSaving.value || !tableRef.value) return
|
||||
const moduleId = String(dataModule.value?.b_id || '').trim()
|
||||
const userId = currentUserId()
|
||||
if (!moduleId || !userId) return
|
||||
|
||||
const current = currentColumnSettings()
|
||||
const delta = columnSettingsDelta(current, columnGlobalRows.value)
|
||||
// 与通用完全一致 → 没有个人覆盖可言,顺手清掉旧记录,避免留下空壳覆盖
|
||||
if (isEmptyColumnDelta(delta)) {
|
||||
if (columnPrefRowExists.value) {
|
||||
columnSaving.value = true
|
||||
try {
|
||||
await removeModulePref(userId, moduleId, PREF_SCHEMA_TYPES.view)
|
||||
columnPrefRowExists.value = false
|
||||
columnBaseline.value = columnRowsOf(listConfig.value)
|
||||
columnDirty.value = false
|
||||
Message.success('已与通用栏位一致,个人设置已清除')
|
||||
} catch (error) {
|
||||
Message.error('清除个人栏位设置失败:' + (error?.message || error))
|
||||
} finally {
|
||||
columnSaving.value = false
|
||||
}
|
||||
} else {
|
||||
columnBaseline.value = columnRowsOf(listConfig.value)
|
||||
columnDirty.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
columnSaving.value = true
|
||||
try {
|
||||
await saveModulePref(userId, moduleId, delta, columnPrefRowExists.value, PREF_SCHEMA_TYPES.view)
|
||||
columnPrefRowExists.value = true
|
||||
// 保存成功即成为新基线:操作条立即消失,不必等整页重载
|
||||
listConfig.value = mergeColumnPref(columnGlobalRows.value, delta)
|
||||
await resetColumns()
|
||||
columnBaseline.value = columnRowsOf(listConfig.value)
|
||||
columnDirty.value = false
|
||||
Message.success('已保存为个人栏位设置')
|
||||
} catch (error) {
|
||||
Message.error('个人栏位设置保存失败:' + (error?.message || error))
|
||||
} finally {
|
||||
columnSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveColumnSettings() {
|
||||
if (loading.value || columnSaving.value || !tableRef.value) return
|
||||
if (typeof tableRef.value.getColumnSettings !== 'function') return
|
||||
@@ -715,7 +821,11 @@ async function saveColumnSettings() {
|
||||
savedListConfig.push({ ...current, b_xh: saved.b_xh, b_width: saved.b_width })
|
||||
}
|
||||
listConfig.value = savedListConfig
|
||||
Message.success('栏位设置已保存')
|
||||
// 通用已是新值:它同时成为「个人增量」的比较基准与新基线
|
||||
columnGlobalRows.value = savedListConfig
|
||||
columnBaseline.value = columnRowsOf(savedListConfig)
|
||||
columnDirty.value = false
|
||||
Message.success('栏位设置已保存为通用')
|
||||
} catch (error) {
|
||||
let errorMessage = error
|
||||
if (error && error.message) errorMessage = error.message
|
||||
@@ -751,9 +861,13 @@ function handleCellChange({ row, field }) {
|
||||
emit('dirty-change', { count: dirtyRows.value.size })
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入目标表:只使用 b_save_table(设计 §6.1)。
|
||||
* 视图表通常不可写,回退到 b_view_table 会让写入在数据库层失败,这里直接返回空串由调用方拒绝。
|
||||
*/
|
||||
function resolveSaveTable() {
|
||||
if (!dataModule.value) throw new Error('数据模块尚未加载')
|
||||
return String(dataModule.value.b_save_table || dataModule.value.b_view_table || '')
|
||||
return String(dataModule.value.b_save_table || '')
|
||||
}
|
||||
|
||||
function dirtyUpdateRows() {
|
||||
@@ -876,8 +990,12 @@ async function saveChanges() {
|
||||
|
||||
async function deleteSelected(rowKeys) {
|
||||
if (saving.value || !rowKeys?.length) return
|
||||
const table = resolveSaveTable()
|
||||
if (!table) {
|
||||
const moduleCode = String(dataModule.value?.b_id || '').trim()
|
||||
if (!moduleCode) {
|
||||
Message.error('该模块未配置模块编码,无法删除')
|
||||
return
|
||||
}
|
||||
if (!resolveSaveTable()) {
|
||||
Message.error('该模块未配置保存表,无法删除')
|
||||
return
|
||||
}
|
||||
@@ -907,13 +1025,14 @@ async function deleteSelected(rowKeys) {
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
await saveObjectApi([
|
||||
{
|
||||
table,
|
||||
key_field: keyField.value,
|
||||
deletes: persistedKeys.map((key) => ({ [keyField.value]: key })),
|
||||
},
|
||||
])
|
||||
// 统一删除接口:传 moduleCode + ids,由后端读取保存表、展开 cascade、
|
||||
// 执行 restrict 与模块 SQL 规则(设计 §6.1)。前端不再拼 saveobjt 的 deletes 行。
|
||||
// 行键即主键值(resolveRowKey 取 keyField 列),直接作为 ids 提交。
|
||||
await deleteApi({
|
||||
target: 'module',
|
||||
moduleCode,
|
||||
ids: persistedKeys.map((key) => String(key)),
|
||||
})
|
||||
// 清除被删行的脏记录
|
||||
const deletedSet = new Set(persistedKeys.map((key) => String(key)))
|
||||
const remaining = new Map()
|
||||
@@ -1018,15 +1137,6 @@ async function handleMoreAction({ key }) {
|
||||
}
|
||||
if (key === 'refresh') {
|
||||
await reload()
|
||||
return
|
||||
}
|
||||
if (key === 'reset-columns') {
|
||||
await resetColumns()
|
||||
Message.success(t('common.columnSettingsReset', '已恢复栏位设置'))
|
||||
return
|
||||
}
|
||||
if (key === 'save-column-settings') {
|
||||
await saveColumnSettings()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,6 +1354,27 @@ defineExpose({
|
||||
/>
|
||||
</div>
|
||||
<div class="list-toolbar__status">{{ statusText }}</div>
|
||||
<!-- 栏位已调整时才浮出的操作条:平时不渲染,因此不占内容区、不改变表格高度。
|
||||
状态用 Tag(胶囊提示)、动作簇用 ButtonGroup(项目统一的贴合按钮组)。 -->
|
||||
<div v-if="columnDirty" class="list-toolbar__group list-column-bar">
|
||||
<Tag variant="secondary">{{ t('common.columnAdjusted', '列宽或顺序已调整') }}</Tag>
|
||||
<ButtonGroup>
|
||||
<Button type="outline" :disabled="columnSaving" @click="undoColumnSettings">
|
||||
<RotateCcw :size="14" /> {{ t('common.undo', '撤销') }}
|
||||
</Button>
|
||||
<Button type="outline" :disabled="columnSaving" @click="saveColumnAsPersonal">
|
||||
{{ t('common.saveAsPersonal', '保存为个人') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="canSaveColumnGlobal"
|
||||
type="primary"
|
||||
:disabled="columnSaving"
|
||||
@click="saveColumnSettings"
|
||||
>
|
||||
{{ t('common.saveAsGlobal', '保存为通用') }}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div class="list-toolbar__group list-toolbar__group--tools">
|
||||
<Button
|
||||
type="outline"
|
||||
@@ -1292,6 +1423,7 @@ defineExpose({
|
||||
@expand="$emit('expand', $event)"
|
||||
@cell-change="handleCellChange"
|
||||
@row-order-change="handleRowOrderChange"
|
||||
@columns-change="handleColumnsChange"
|
||||
>
|
||||
<template v-for="slotName in cellSlotNames" :key="slotName" #[slotName]="slotProps">
|
||||
<slot :name="slotName" v-bind="slotProps" />
|
||||
@@ -1476,6 +1608,14 @@ defineExpose({
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 栏位已调整操作条:仅在 columnDirty 时渲染,不占常驻空间。
|
||||
容器不画框、不填色 —— 状态由 Tag 承担、动作由 ButtonGroup 承担,
|
||||
样式(圆角 / 描边 / 间距 / 主题色)全部来自组件库,跟随主题且不会与项目风格脱节。 */
|
||||
.list-column-bar {
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 底部分页条:左总数、右分页器 */
|
||||
.list-footer {
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup>
|
||||
import { computed, h, ref, shallowRef, useSlots, watch } from 'vue'
|
||||
import { Checkbox } from '@/components/ui'
|
||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||
@@ -31,6 +31,8 @@ const emit = defineEmits([
|
||||
'expand',
|
||||
'cell-change',
|
||||
'row-order-change',
|
||||
// 列宽/列顺序变化:栏位设置可能已偏离基线,父级据此刷新「已调整」状态
|
||||
'columns-change',
|
||||
])
|
||||
const i18nStore = useI18nStore()
|
||||
const { t } = i18nStore
|
||||
@@ -443,6 +445,16 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* FmsTable 回传列宽/列序(v-model:columns 的写入口)。
|
||||
* 显式接管而不直接用 v-model,是为了在写回的同时把「列已变化」上抛:
|
||||
* 父级据此判断栏位设置是否已偏离基线(配置变化触发的 rebuild 不走这里,不会误报)。
|
||||
*/
|
||||
function onColumnsUpdate(nextColumns) {
|
||||
columns.value = nextColumns
|
||||
emit('columns-change', getColumnSettings())
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetColumns,
|
||||
refreshLayout,
|
||||
@@ -457,7 +469,8 @@ defineExpose({
|
||||
<template>
|
||||
<FmsTable
|
||||
ref="tableRef"
|
||||
v-model:columns="columns"
|
||||
:columns="columns"
|
||||
@update:columns="onColumnsUpdate"
|
||||
:data-source="dataSource"
|
||||
:footer-data="footerData"
|
||||
:row-key="rowKey"
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
// 查询布局读写与两层继承合并(系统默认 → 个人)。
|
||||
// 布局读写与两层继承合并(系统默认 → 个人)。
|
||||
// s_module_schema —— 系统默认。查询布局以 query JSON(quick 列表 + 条件)为来源;
|
||||
// 由 schemaRender.applyQueryLayout 在「保存为通用」时回写。
|
||||
// 列宽顺序以 view JSON(columns 节点)为来源。
|
||||
// 分别由 schemaRender.applyQueryLayout / applyColumnSettings 在
|
||||
// 「保存为通用」时回写。
|
||||
// s_user_module_pref —— 个人覆盖(b_schema_type + b_schema_json),只存相对系统默认的增量。
|
||||
// 布局只能调整区域与顺序,不得放宽字段权限——合并后仍按字段权限裁剪(开发规范 通用原则 10)。
|
||||
// 两种布局(query 查询布局 / view 栏位布局)共用本文件的读写与增量计算,靠 schemaType 区分。
|
||||
// 布局只能调整区域、顺序与列宽,不得放宽字段权限——合并后仍按字段权限裁剪(开发规范 通用原则 10)。
|
||||
import { loadDataApi, saveObjectApi } from '@/services/api'
|
||||
import { escapeSqlText } from '@/components/fms-module-common/queryUtils'
|
||||
import { formatLocalDateTime } from '@/components/fms-module-common/fieldUtils'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const SCHEMA_TYPE = 'query'
|
||||
/** 个人偏好层可记录的布局类型:query=查询布局,view=列表栏位布局 */
|
||||
export const PREF_SCHEMA_TYPES = { query: 'query', view: 'view' }
|
||||
const DEFAULT_SCHEMA_TYPE = PREF_SCHEMA_TYPES.query
|
||||
const USER_PREF_TABLE = 's_user_module_pref'
|
||||
const USER_PREF_KEY = 'b_user_id,b_module_id,b_schema_type'
|
||||
|
||||
@@ -86,11 +91,126 @@ export function layoutDelta(current, base) {
|
||||
|
||||
/** 增量是否为空(无实际覆盖则无需落库) */
|
||||
export function isEmptyDelta(delta) {
|
||||
return !delta || (Object.keys(delta.area || {}).length === 0 && Object.keys(delta.order || {}).length === 0)
|
||||
return (
|
||||
!delta ||
|
||||
(Object.keys(delta.area || {}).length === 0 && Object.keys(delta.order || {}).length === 0)
|
||||
)
|
||||
}
|
||||
|
||||
/* ================= 栏位布局(view)个人层 =================
|
||||
* 与查询布局同构:系统默认(s_module_schema.view 的 columns 节点顺序与 width)
|
||||
* → 个人层增量(s_user_module_pref, b_schema_type='view')。
|
||||
*
|
||||
* 增量结构:{ schemaVersion: 1, columns: { [b_field]: { width } }, order: [b_field, ...] }
|
||||
* - columns:逐字段只记「与通用不同的宽度」,同宽不记,保证通用改宽仍能透传;
|
||||
* - order :顺序是全局排列,无法只记「谁和谁换了位」,因此整体记当前顺序。
|
||||
* 个人层只调整顺序与列宽,不得增删字段(开发规范 通用原则 10)。
|
||||
*/
|
||||
|
||||
/** 栏位行 → 规范化 { b_field, b_width }(忽略无字段的行) */
|
||||
function normalizeColumnRows(rows) {
|
||||
return (rows || [])
|
||||
.filter((row) => row && String(row.b_field ?? '') !== '')
|
||||
.map((row) => ({ b_field: String(row.b_field), b_width: row.b_width }))
|
||||
}
|
||||
|
||||
/** 宽度归一:null / undefined / '' / 非数字一律视为「未设置」,与数字 0 区分开 */
|
||||
function widthOf(raw) {
|
||||
if (raw == null || raw === '') return null
|
||||
const value = Number(raw)
|
||||
return Number.isFinite(value) ? value : null
|
||||
}
|
||||
|
||||
/** 空对象常量:避免逐处内联空字面量 */
|
||||
const emptyColumns = Object.freeze({})
|
||||
|
||||
/**
|
||||
* 个人层覆盖叠加到通用栏位之上,返回新的栏位行数组。
|
||||
* 个人只改宽度与顺序,不新增字段:通用里没有的字段一律忽略。
|
||||
* @param baseRows 通用栏位行(buildListConfig 结果,含 b_visible/options 等)
|
||||
* @param pref 个人层增量(可能为 null)
|
||||
*/
|
||||
export function mergeColumnPref(baseRows, pref) {
|
||||
const base = normalizeColumnRows(baseRows)
|
||||
if (!pref) return baseRows || []
|
||||
const widthById = pref.columns || {}
|
||||
const orderList = Array.isArray(pref.order) ? pref.order.map(String) : []
|
||||
|
||||
const known = new Set(base.map((row) => row.b_field))
|
||||
const rank = new Map()
|
||||
orderList.forEach((field, index) => {
|
||||
if (known.has(field)) rank.set(field, index)
|
||||
})
|
||||
|
||||
// 有 order 名次的按名次排;没有的(含未列入 order 的新字段)保持原有相对顺序追加在后
|
||||
const ordered = base
|
||||
.map((row, index) => ({ row, index }))
|
||||
.toSorted((a, b) => {
|
||||
const ra = rank.has(a.row.b_field) ? rank.get(a.row.b_field) : Number.MAX_SAFE_INTEGER
|
||||
const rb = rank.has(b.row.b_field) ? rank.get(b.row.b_field) : Number.MAX_SAFE_INTEGER
|
||||
return ra === rb ? a.index - b.index : ra - rb
|
||||
})
|
||||
.map((item) => item.row)
|
||||
|
||||
// 回到 baseRows 的原始形态(保留 b_visible/options 等),只改顺序与宽度并重编 b_xh
|
||||
const rowByField = new Map((baseRows || []).map((row) => [String(row?.b_field ?? ''), row]))
|
||||
return ordered.map((item, index) => {
|
||||
const original = rowByField.get(item.b_field) || item
|
||||
const override = widthById[item.b_field]
|
||||
const nextWidth = override?.width != null ? widthOf(override.width) : widthOf(original.b_width)
|
||||
return { ...original, b_xh: (index + 1) * 10, b_width: nextWidth }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前栏位设置相对通用默认的增量(只保留差异)。
|
||||
* @param current getColumnSettings() 结果([{ b_field, b_xh, b_width }])
|
||||
* @param base 通用栏位行(含 b_field / b_width,数组顺序即通用顺序)
|
||||
*/
|
||||
export function columnSettingsDelta(current, base) {
|
||||
const baseRows = normalizeColumnRows(base)
|
||||
const curRows = normalizeColumnRows(current)
|
||||
const baseWidth = new Map(baseRows.map((row) => [row.b_field, widthOf(row.b_width)]))
|
||||
|
||||
// 宽度:只记与通用不同、且通用里确实存在的字段
|
||||
const columns = {}
|
||||
for (const row of curRows) {
|
||||
if (!baseWidth.has(row.b_field)) continue
|
||||
const next = widthOf(row.b_width)
|
||||
if (next == null || next === baseWidth.get(row.b_field)) continue
|
||||
columns[row.b_field] = { width: next }
|
||||
}
|
||||
|
||||
// 顺序:与通用一致时不记,避免把通用顺序固化、导致管理员后续调整无法透传
|
||||
const baseOrder = baseRows.map((row) => row.b_field)
|
||||
const curOrder = curRows.map((row) => row.b_field)
|
||||
const sameOrder =
|
||||
baseOrder.length === curOrder.length && baseOrder.every((field, i) => field === curOrder[i])
|
||||
|
||||
const delta = { schemaVersion: 1, columns }
|
||||
if (!sameOrder) delta.order = curOrder
|
||||
return delta
|
||||
}
|
||||
|
||||
/** 栏位增量是否为空(宽度与顺序都与通用一致 → 无需落库,可清空个人层) */
|
||||
export function isEmptyColumnDelta(delta) {
|
||||
if (!delta) return true
|
||||
return Object.keys(delta.columns ?? emptyColumns).length === 0 && !delta.order
|
||||
}
|
||||
|
||||
/** 当前栏位设置与基线是否一致(列宽或顺序任一不同即为「已调整」) */
|
||||
export function sameColumnSettings(current, baseline) {
|
||||
const cur = normalizeColumnRows(current)
|
||||
const base = normalizeColumnRows(baseline)
|
||||
if (cur.length !== base.length) return false
|
||||
return cur.every((row, index) => {
|
||||
const other = base[index]
|
||||
return row.b_field === other.b_field && widthOf(row.b_width) === widthOf(other.b_width)
|
||||
})
|
||||
}
|
||||
|
||||
/** 读取个人层(s_user_module_pref)某一 schema 类型的覆盖 JSON;无记录或解析失败返回 null */
|
||||
export async function loadModulePref(userId, moduleId, schemaType = SCHEMA_TYPE) {
|
||||
export async function loadModulePref(userId, moduleId, schemaType = DEFAULT_SCHEMA_TYPE) {
|
||||
if (!userId || !moduleId) return null
|
||||
const condition =
|
||||
`b_user_id = N'${escapeSqlText(userId)}' AND b_module_id = N'${escapeSqlText(moduleId)}' ` +
|
||||
@@ -116,7 +236,13 @@ function parseRow(row) {
|
||||
/**
|
||||
* 写个人层覆盖(s_user_module_pref)。后端 saveobject 无 upsert:insert 冲突重试 updates。
|
||||
*/
|
||||
export async function saveModulePref(userId, moduleId, pref, rowExists, schemaType = SCHEMA_TYPE) {
|
||||
export async function saveModulePref(
|
||||
userId,
|
||||
moduleId,
|
||||
pref,
|
||||
rowExists,
|
||||
schemaType = DEFAULT_SCHEMA_TYPE,
|
||||
) {
|
||||
if (!userId || !moduleId) return false
|
||||
const row = {
|
||||
b_user_id: userId,
|
||||
@@ -143,7 +269,7 @@ export async function saveModulePref(userId, moduleId, pref, rowExists, schemaTy
|
||||
* 删除某层覆盖(「恢复默认」即删除个人层,回落到系统/企业层或系统默认)。
|
||||
* schemaType 为空数组则删除该模块全部类型(供「恢复出厂」等管理操作使用)。
|
||||
*/
|
||||
export async function removeModulePref(userId, moduleId, schemaType = SCHEMA_TYPE) {
|
||||
export async function removeModulePref(userId, moduleId, schemaType = DEFAULT_SCHEMA_TYPE) {
|
||||
if (!userId || !moduleId) return false
|
||||
const keys = Array.isArray(schemaType) ? schemaType : [schemaType]
|
||||
const deletes = keys.map((type) => ({
|
||||
@@ -171,4 +297,4 @@ export async function loadFieldPower(userId, moduleId) {
|
||||
power.set(String(row.b_field_id), Number(row.b_query) === 0 ? 0 : 1)
|
||||
}
|
||||
return power
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user