20260923230500
This commit is contained in:
1 parent
e6935b8fa4
commit
0405cd198a
36 files changed
+3668
-911
No files matched your search
@@ -28,6 +28,21 @@ export function queryItemLabel(item, translate) {
|
||||
return typeof translate === 'function' ? translate(field.b_i18n, fallback) : fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键词输入框占位文案:按可搜索字段名在运行时生成,不写进模块配置。
|
||||
* 字段多于 5 个时只列前 5 个,末尾加「等」。
|
||||
* @param items 关键词字段项(queryItem[])
|
||||
* @param translate 多语言函数(t(key, fallback)),缺省用中文默认文案
|
||||
*/
|
||||
export function buildKeywordPlaceholder(items, translate) {
|
||||
const labels = (items || []).map((item) => queryItemLabel(item, translate)).filter(Boolean)
|
||||
if (!labels.length) return ''
|
||||
const text = (key, fallback) => (typeof translate === 'function' ? translate(key, fallback) : fallback)
|
||||
const head = labels.slice(0, 5).join('、')
|
||||
const tail = labels.length > 5 ? ` ${text('common.etc', '等')}` : ''
|
||||
return `${text('common.keywordSearch', '搜索')} ${head}${tail}`
|
||||
}
|
||||
|
||||
export function isRangeQuery(item) {
|
||||
return (
|
||||
item.config.b_operator === 'between' ||
|
||||
@@ -111,7 +126,8 @@ export function sqlLiteral(value, field, endOfRange = false) {
|
||||
/* ---------- 统一查询条件:AST → SQL ----------
|
||||
* 页签条件、关键词、快捷筛选、更多筛选都归一到同一套结构,最终由 astToSql 生成 SQL:
|
||||
* 条件 { type: 'condition', field, operator, value, join }
|
||||
* 条件组 { type: 'group', children: [条件 | 条件组], logic? }
|
||||
* 条件组 { type: 'group', children: [条件 | 条件组 | 原始片段], logic? }
|
||||
* 原始片段 { type: 'raw', sql }(页签条件由配置人员直接写 SQL,整段作为一个子项参与连接)
|
||||
* join 表示「本项与上一相邻项如何连接」(且 / 或);首项 join 忽略。
|
||||
* 页签 / 快捷筛选 / 更多筛选同层一律「且」,只有关键词在多个字段之间取「或」。
|
||||
*/
|
||||
@@ -336,6 +352,23 @@ export function buildConditionSql(node, fieldMap) {
|
||||
return `${column} ${operator === 'ne' ? '<>' : '='} ${literal}`
|
||||
}
|
||||
|
||||
/* 原始 SQL 片段 → SQL:整段加括号,保证它作为子项参与连接时优先级独立。
|
||||
* 不做任何改写(分号 / 注释由配置面板拦截):运行时静默丢弃条件会把它放宽成「更多数据」,
|
||||
* 比原样执行更危险。 */
|
||||
function buildRawSql(node) {
|
||||
const sql = String(node?.sql ?? '').trim()
|
||||
return sql ? `(${sql})` : ''
|
||||
}
|
||||
|
||||
/** 页签条件 SQL 的基础校验:只防手误(分号、注释),不当安全边界 */
|
||||
export function validateConditionSql(sql) {
|
||||
const text = String(sql ?? '').trim()
|
||||
if (!text) return true
|
||||
if (text.includes(';')) return '不能包含分号'
|
||||
if (text.includes('--') || text.includes('/*')) return '不能包含注释'
|
||||
return true
|
||||
}
|
||||
|
||||
/** 条件组 → SQL(递归)。子项自带 join 时按各自连接符拼接:
|
||||
* 全部同向 → 简单连接;混合 → 从左到右逐项加括号,与界面阅读顺序一致
|
||||
* (a 且 b 或 c = (a 且 b) 或 c)。
|
||||
@@ -345,7 +378,11 @@ export function buildGroupSql(group, fieldMap) {
|
||||
const parts = []
|
||||
for (const child of group.children || []) {
|
||||
const sql =
|
||||
child?.type === 'group' ? buildGroupSql(child, fieldMap) : buildConditionSql(child, fieldMap)
|
||||
child?.type === 'group'
|
||||
? buildGroupSql(child, fieldMap)
|
||||
: child?.type === 'raw'
|
||||
? buildRawSql(child)
|
||||
: buildConditionSql(child, fieldMap)
|
||||
if (sql) parts.push({ sql, join: child.join })
|
||||
}
|
||||
if (!parts.length) return ''
|
||||
@@ -373,6 +410,7 @@ export function buildGroupSql(group, fieldMap) {
|
||||
export function astToSql(ast, fieldMap) {
|
||||
if (!ast) return ''
|
||||
if (ast.type === 'group') return buildGroupSql(ast, fieldMap)
|
||||
if (ast.type === 'raw') return buildRawSql(ast)
|
||||
return buildConditionSql(ast, fieldMap)
|
||||
}
|
||||
|
||||
@@ -395,13 +433,14 @@ export function buildQuickAst(items, values) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作队列页签 → AST。
|
||||
* 页签条件是配置里写好的完整条件(含日期预设),直接沿用 AST 形状交给 astToSql,
|
||||
* 因此页签不需要第二套 SQL 生成逻辑。
|
||||
* @param view query JSON 中的页签节点({ conditions: [...] })
|
||||
* @param fieldMap 「字段编码 → s_field 行」的 Map;字段缺失的条件直接丢弃
|
||||
* 工作队列页签 → AST:条件为配置直写的 SQL 片段(conditionSql),整段作为 raw 子项参与连接。
|
||||
* 存量页签的结构化 conditions 继续兼容(含日期语义值),因此不需要第二套 SQL 生成逻辑。
|
||||
* @param view query JSON 中的页签节点({ conditionSql } 或 { conditions: [...] })
|
||||
* @param fieldMap 「字段编码 → s_field 行」的 Map;旧结构里字段缺失的条件直接丢弃
|
||||
*/
|
||||
export function buildViewAst(view, fieldMap) {
|
||||
const sql = String(view?.conditionSql ?? '').trim()
|
||||
if (sql) return { type: 'raw', sql }
|
||||
const children = []
|
||||
for (const condition of view?.conditions || []) {
|
||||
if (!condition || condition.field == null) continue
|
||||
|
||||
@@ -131,14 +131,14 @@ export function buildEditRender(editSchema) {
|
||||
}
|
||||
|
||||
/**
|
||||
* query JSON(schemaVersion 2)→ 查询入口渲染结构。
|
||||
* query JSON(schemaVersion 3)→ 查询入口渲染结构。
|
||||
*
|
||||
* 产出(运行时组件只消费这一份,不再感知 JSON 节点差异):
|
||||
* - views 工作队列页签(按 order 排序)
|
||||
* - defaultViewCode 模块配置的默认页签编码
|
||||
* - keywordSearch 关键词搜索(占位文案 / 触发方式 / 字段项),未配置时为 null
|
||||
* - views 工作队列页签(按 order 排序,带分组;组内单选、组间条件叠加)
|
||||
* - defaultViewCodes 分组 → 默认页签编码(每组至多一个)
|
||||
* - keywordSearch 关键词搜索(字段项;占位文案由调用方按字段名运行时生成),未配置时为 null
|
||||
* - quickFilters 快捷筛选字段项(按 order 排序)
|
||||
* - filterGroups 更多筛选分组(组内字段项按配置顺序)
|
||||
* - moreFilters 更多筛选字段项(字段查询元数据全集,已进快捷筛选的不重复出现)
|
||||
*
|
||||
* 字段项(queryItem)统一为 { field, config, options, optionConfig },与 FmsQueryControl /
|
||||
* buildQuickAst 消费的形状一致;字段级算子只在 queryFields 里定义一处。
|
||||
@@ -177,31 +177,43 @@ export function buildQueryRender(querySchema, fields, allowedFields = null) {
|
||||
const views = []
|
||||
for (const [index, view] of (querySchema?.views || []).entries()) {
|
||||
if (!view || view.code == null) continue
|
||||
const conditionSql = String(view.conditionSql ?? '').trim()
|
||||
const conditions = (view.conditions || []).filter(
|
||||
(condition) => condition && condition.field != null,
|
||||
)
|
||||
// 条件字段不可用(未定义 / 无查询权限)时丢弃整个页签:静默丢单个条件会把
|
||||
// 「待处理」悄悄放宽成更多数据,与页签语义不符。
|
||||
if (conditions.some((condition) => !itemOf(condition.field))) continue
|
||||
// 旧结构(结构化条件)里条件字段不可用(未定义 / 无查询权限)时丢弃整个页签:静默丢单个
|
||||
// 条件会把「待处理」悄悄放宽成更多数据,与页签语义不符。
|
||||
// SQL 条件无法解析字段引用,原样保留(由配置人员核对)。
|
||||
if (!conditionSql && conditions.some((condition) => !itemOf(condition.field))) continue
|
||||
views.push({
|
||||
code: String(view.code),
|
||||
label: String(view.label ?? view.code),
|
||||
group: String(view.group ?? ''),
|
||||
order: Number(view.order) || (index + 1) * 10,
|
||||
showCount: view.showCount === true,
|
||||
isDefault: view.isDefault === true,
|
||||
conditionSql,
|
||||
conditions,
|
||||
})
|
||||
}
|
||||
views.sort((a, b) => a.order - b.order)
|
||||
|
||||
const keywordNode = querySchema?.keywordSearch || null
|
||||
const keywordItems = (keywordNode?.fields || []).map(itemOf).filter(Boolean)
|
||||
const keywordSearch = keywordItems.length
|
||||
? {
|
||||
placeholder: String(keywordNode.placeholder || ''),
|
||||
trigger: keywordNode.trigger === 'debounce' ? 'debounce' : 'enter_or_debounce',
|
||||
items: keywordItems,
|
||||
}
|
||||
: null
|
||||
// 默认页签按组给:显式 isDefault 优先(每组取第一个);老数据只有顶层 defaultViewCode,
|
||||
// 按其所在组补一个默认,保证老配置切过去仍落在原页签上
|
||||
const defaultViewCodes = {}
|
||||
for (const view of views) {
|
||||
if (!view.isDefault || defaultViewCodes[view.group] != null) continue
|
||||
defaultViewCodes[view.group] = view.code
|
||||
}
|
||||
const legacyDefaultCode = String(querySchema?.defaultViewCode ?? '')
|
||||
if (legacyDefaultCode && !views.some((view) => view.isDefault)) {
|
||||
const target = views.find((view) => view.code === legacyDefaultCode)
|
||||
if (target) defaultViewCodes[target.group] = target.code
|
||||
}
|
||||
|
||||
// 关键词:字段清单来自配置,占位文案与触发方式都在运行时定(不入配置)
|
||||
const keywordItems = (querySchema?.keywordSearch?.fields || []).map(itemOf).filter(Boolean)
|
||||
const keywordSearch = keywordItems.length ? { items: keywordItems } : null
|
||||
|
||||
const quickFilters = (querySchema?.quickFilters || [])
|
||||
.map((entry, index) => {
|
||||
@@ -212,27 +224,14 @@ export function buildQueryRender(querySchema, fields, allowedFields = null) {
|
||||
.filter(Boolean)
|
||||
.toSorted((a, b) => a.order - b.order)
|
||||
|
||||
// 已进快捷筛选的字段不再进更多筛选:同一条条件不给两个入口
|
||||
// 更多筛选:字段查询元数据全集(已进快捷筛选的字段不重复出现:同一条条件不给两个入口),
|
||||
// 顺序即配置顺序,不再需要人工分组
|
||||
const quickFieldNames = new Set(quickFilters.map((item) => String(item.field.b_field)))
|
||||
const filterGroups = (querySchema?.filterGroups || [])
|
||||
.map((group, index) => ({
|
||||
code: String(group?.code ?? `group${index + 1}`),
|
||||
label: String(group?.label ?? ''),
|
||||
order: Number(group?.order) || (index + 1) * 10,
|
||||
items: (group?.fields || [])
|
||||
.map(itemOf)
|
||||
.filter((item) => item && !quickFieldNames.has(String(item.field.b_field))),
|
||||
}))
|
||||
.filter((group) => group.items.length)
|
||||
.toSorted((a, b) => a.order - b.order)
|
||||
const moreFilters = Object.keys(queryFields)
|
||||
.map(itemOf)
|
||||
.filter((item) => item && !quickFieldNames.has(String(item.field.b_field)))
|
||||
|
||||
return {
|
||||
views,
|
||||
defaultViewCode: String(querySchema?.defaultViewCode ?? views[0]?.code ?? ''),
|
||||
keywordSearch,
|
||||
quickFilters,
|
||||
filterGroups,
|
||||
}
|
||||
return { views, defaultViewCodes, keywordSearch, quickFilters, moreFilters }
|
||||
}
|
||||
|
||||
/** 节点(含分组子树)在 settings 顺序中的最小序号,用于同层稳定排序 */
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 更多筛选(抽屉):按模块配置的字段分组铺开完整筛选字段,控件按字段类型渲染。
|
||||
* 字段清单、分组、顺序、算子全部来自查询配置 —— 页面与本组件都不内置模块专用选项,
|
||||
* 也不再提供自由拼装条件行/条件组的入口(技术表达式不由业务人员拼)。
|
||||
* 更多筛选(抽屉):铺开未被快捷筛选占用的其余查询字段,控件按字段类型渲染。
|
||||
* 字段清单、顺序、算子全部来自查询配置,按配置顺序平铺(不再人工分组)——
|
||||
* 页面与本组件都不内置模块专用选项,也不提供自由拼装条件行/条件组的入口。
|
||||
*
|
||||
* 抽屉内编辑的只是草稿:「应用筛选」才生效并回到第一页,「取消」丢弃改动(由页面重开时重置草稿),
|
||||
* 「重置」只清空抽屉里的草稿值,同样需要「应用筛选」才落到列表。
|
||||
@@ -23,8 +23,8 @@ import { useI18nStore } from '@/stores/i18n'
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
// 更多筛选分组:[{ code, label, order, items: queryItem[] }]
|
||||
groups: { type: Array, default: () => [] },
|
||||
// 更多筛选字段项(扁平,按配置顺序;已进快捷筛选的字段不会出现)
|
||||
items: { type: Array, default: () => [] },
|
||||
// 抽屉草稿:{ [b_field]: { operator, value } }
|
||||
modelValue: { type: Object, default: () => ({}) },
|
||||
})
|
||||
@@ -104,14 +104,10 @@ function labelOf(item) {
|
||||
}
|
||||
|
||||
/** 草稿里已填写的条件数(页脚提示用) */
|
||||
const draftCount = computed(() =>
|
||||
flattenItems(props.groups).filter((item) => hasConditionValue(entryOf(item).value)).length,
|
||||
const draftCount = computed(
|
||||
() => (props.items || []).filter((item) => hasConditionValue(entryOf(item).value)).length,
|
||||
)
|
||||
|
||||
function flattenItems(groups) {
|
||||
return (groups || []).flatMap((group) => group.items || [])
|
||||
}
|
||||
|
||||
function reset() {
|
||||
emit('update:modelValue', {})
|
||||
}
|
||||
@@ -131,41 +127,36 @@ function close() {
|
||||
@update:open="emit('update:open', $event)"
|
||||
@cancel="close"
|
||||
>
|
||||
<div v-if="groups.length" class="filter-groups">
|
||||
<section v-for="group in groups" :key="group.code" class="filter-section">
|
||||
<h4 v-if="group.label" class="filter-section__title">{{ group.label }}</h4>
|
||||
<div class="filter-section__body">
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="item.field.b_field"
|
||||
class="filter-row"
|
||||
:class="{ 'filter-row--wide-value': needsWideValue(item) }"
|
||||
>
|
||||
<span class="filter-row__label" :title="labelOf(item)">{{ labelOf(item) }}</span>
|
||||
<Select
|
||||
class="filter-row__operator"
|
||||
:model-value="entryOf(item).operator"
|
||||
:options="operatorOptions(item)"
|
||||
:filterable="false"
|
||||
@update:model-value="setOperator(item, $event)"
|
||||
/>
|
||||
<FmsQueryControl
|
||||
v-if="needsValueControl(item)"
|
||||
class="filter-row__value"
|
||||
:model-value="entryOf(item).value"
|
||||
:item="controlItemOf(item)"
|
||||
date-presets
|
||||
compact
|
||||
@update:model-value="setValue(item, $event)"
|
||||
/>
|
||||
<span v-else class="filter-row__value filter-row__value--empty">
|
||||
{{ t('common.noValueNeeded', '无需填写值') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div v-if="items.length" class="filter-list">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.field.b_field"
|
||||
class="filter-row"
|
||||
:class="{ 'filter-row--wide-value': needsWideValue(item) }"
|
||||
>
|
||||
<span class="filter-row__label" :title="labelOf(item)">{{ labelOf(item) }}</span>
|
||||
<Select
|
||||
class="filter-row__operator"
|
||||
:model-value="entryOf(item).operator"
|
||||
:options="operatorOptions(item)"
|
||||
:filterable="false"
|
||||
@update:model-value="setOperator(item, $event)"
|
||||
/>
|
||||
<FmsQueryControl
|
||||
v-if="needsValueControl(item)"
|
||||
class="filter-row__value"
|
||||
:model-value="entryOf(item).value"
|
||||
:item="controlItemOf(item)"
|
||||
date-presets
|
||||
compact
|
||||
@update:model-value="setValue(item, $event)"
|
||||
/>
|
||||
<span v-else class="filter-row__value filter-row__value--empty">
|
||||
{{ t('common.noValueNeeded', '无需填写值') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="filter-groups-empty">
|
||||
<div v-else class="filter-list-empty">
|
||||
{{ t('common.noMoreFilters', '本模块暂未配置更多筛选字段') }}
|
||||
</div>
|
||||
|
||||
@@ -187,20 +178,7 @@ function close() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.filter-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.filter-section__title {
|
||||
margin: 0 0 6px;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-section__body {
|
||||
.filter-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -254,7 +232,7 @@ function close() {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-groups-empty {
|
||||
.filter-list-empty {
|
||||
padding: 24px 0;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: var(--fms-font-size);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 关键词搜索:对模块配置的可搜索字段做跨字段模糊匹配(字段之间取「或」)。
|
||||
* 不要求用户先选字段,字段清单与占位文案全部来自模块查询配置。
|
||||
* 不要求用户先选字段;字段清单来自模块查询配置,占位文案由字段名在运行时生成(不入配置)。
|
||||
*
|
||||
* 触发方式由配置给出:
|
||||
* - enter_or_debounce:回车立即查询,停止输入 400ms 后自动查询(默认)
|
||||
* - enter:只认回车
|
||||
* - debounce:只认防抖
|
||||
* 触发固定为「回车立即查询 + 停止输入 400ms 后自动查询」;
|
||||
* 清空输入即立即查询(不必再按回车),避免留下一个「看不出为什么还是旧结果」的空条件。
|
||||
*/
|
||||
import { computed, onBeforeUnmount, watch } from 'vue'
|
||||
import { onBeforeUnmount, watch } from 'vue'
|
||||
import { Search } from '@lucide/vue'
|
||||
import { Input } from '@/components/ui'
|
||||
|
||||
@@ -18,15 +15,11 @@ const DEBOUNCE_MS = 400
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
trigger: { type: String, default: 'enter_or_debounce' },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'search'])
|
||||
|
||||
let debounceTimer = null
|
||||
|
||||
const enterEnabled = computed(() => props.trigger !== 'debounce')
|
||||
const debounceEnabled = computed(() => props.trigger !== 'enter')
|
||||
|
||||
function clearTimer() {
|
||||
if (!debounceTimer) return
|
||||
clearTimeout(debounceTimer)
|
||||
@@ -35,13 +28,11 @@ function clearTimer() {
|
||||
|
||||
function onInput(value) {
|
||||
emit('update:modelValue', value)
|
||||
if (!debounceEnabled.value) return
|
||||
clearTimer()
|
||||
debounceTimer = setTimeout(() => emit('search', value), DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
function onEnter() {
|
||||
if (!enterEnabled.value) return
|
||||
clearTimer()
|
||||
emit('search', props.modelValue)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
astToSql,
|
||||
buildFilterStateAst,
|
||||
buildKeywordAst,
|
||||
buildKeywordPlaceholder,
|
||||
buildQuickAst,
|
||||
buildViewAst,
|
||||
combineAst,
|
||||
@@ -43,7 +44,7 @@ import {
|
||||
loadFieldPower,
|
||||
loadModulePref,
|
||||
mergeColumnPref,
|
||||
mergeDefaultViewCode,
|
||||
mergeDefaultViewCodes,
|
||||
removeModulePref,
|
||||
sameColumnSettings,
|
||||
saveModulePref,
|
||||
@@ -103,18 +104,18 @@ const dataSource = shallowRef([])
|
||||
const grandTotals = shallowRef({})
|
||||
|
||||
// ── 查询入口状态 ──
|
||||
// 查询入口配置(s_module_schema.query,schemaVersion 2):页签 / 关键词 / 快捷筛选 / 更多筛选分组
|
||||
// 查询入口配置(s_module_schema.query,schemaVersion 3):页签(可分组)/ 关键词 / 快捷筛选 / 更多筛选
|
||||
const queryRender = shallowRef({
|
||||
views: [],
|
||||
defaultViewCode: '',
|
||||
defaultViewCodes: {},
|
||||
keywordSearch: null,
|
||||
quickFilters: [],
|
||||
filterGroups: [],
|
||||
moreFilters: [],
|
||||
})
|
||||
// 当前页签(工作队列视图)
|
||||
const viewCode = ref('')
|
||||
// 模块配置的默认页签(系统默认层)
|
||||
const defaultViewCode = ref('')
|
||||
// 当前选中的页签(分组 → 编码;每组可单独取消,组间条件叠加)
|
||||
const viewCodes = ref({})
|
||||
// 模块配置的默认页签(系统默认层,分组 → 编码)
|
||||
const defaultViewCodes = ref({})
|
||||
const keyword = ref('')
|
||||
// 快捷筛选:草稿(界面绑定)与生效值分开,点「查询」才落到生效
|
||||
const quickValues = ref({})
|
||||
@@ -131,7 +132,7 @@ let countsToken = 0
|
||||
const queryAst = shallowRef(null)
|
||||
// 个人层默认页签偏好(s_user_module_pref, b_schema_type='query')
|
||||
const prefRowExists = ref(false)
|
||||
const savedDefaultViewCode = ref('')
|
||||
const savedDefaultViewCodes = ref({})
|
||||
|
||||
// ── 栏位设置(列宽/顺序)──
|
||||
// 基线:进入本页(或保存/重载)时的栏位快照,用于判断「列是否已被调整」。
|
||||
@@ -256,7 +257,7 @@ const moreMenuItems = computed(() => [
|
||||
key: 'set-default-view',
|
||||
label: t('common.setDefaultView', '将当前页签设为我的默认'),
|
||||
icon: h(Star, { size: 14 }),
|
||||
disabled: !dataModule.value || !viewCode.value,
|
||||
disabled: !dataModule.value || !Object.keys(viewCodes.value).length,
|
||||
},
|
||||
{
|
||||
key: 'custom-export',
|
||||
@@ -275,17 +276,19 @@ const cellSlotNames = computed(() => Object.keys(slots).filter((name) => name.st
|
||||
// ── 查询入口派生 ──
|
||||
const views = computed(() => queryRender.value.views)
|
||||
const keywordConfig = computed(() => queryRender.value.keywordSearch)
|
||||
/** 关键词输入框占位文案:按可搜索字段名运行时生成(配置里不保存文案) */
|
||||
const keywordPlaceholder = computed(() =>
|
||||
buildKeywordPlaceholder(keywordConfig.value?.items || [], t),
|
||||
)
|
||||
const quickItems = computed(() => queryRender.value.quickFilters)
|
||||
const filterGroups = computed(() => queryRender.value.filterGroups)
|
||||
const moreFilters = computed(() => queryRender.value.moreFilters)
|
||||
// 字段编码 → 查询条件项:条件回显文案与更多筛选都从这里取字段元数据
|
||||
const queryItemByField = computed(() => {
|
||||
const map = new Map()
|
||||
const push = (item) => map.set(String(item.field.b_field), item)
|
||||
for (const item of quickItems.value) push(item)
|
||||
for (const item of keywordConfig.value?.items || []) push(item)
|
||||
for (const group of filterGroups.value) {
|
||||
for (const item of group.items || []) push(item)
|
||||
}
|
||||
for (const item of moreFilters.value) push(item)
|
||||
return map
|
||||
})
|
||||
|
||||
@@ -480,15 +483,18 @@ async function loadModuleConfig() {
|
||||
columnDirty.value = false
|
||||
|
||||
// 查询入口:默认页签(系统默认 ← 个人覆盖)与生效条件。切模块即回到配置的默认页签,
|
||||
// 个人层只允许挑一个已存在且有权限的页签,不能凭空造页签。
|
||||
// 个人层只能在该组已存在且有权限的页签里挑,不能凭空造页签。
|
||||
queryRender.value = render
|
||||
defaultViewCode.value = render.defaultViewCode
|
||||
savedDefaultViewCode.value = String(savedPref?.defaultViewCode || '')
|
||||
defaultViewCodes.value = { ...render.defaultViewCodes }
|
||||
savedDefaultViewCodes.value = {
|
||||
...(savedPref?.defaultViewCodes ||
|
||||
(savedPref?.defaultViewCode ? { '': String(savedPref.defaultViewCode) } : {})),
|
||||
}
|
||||
prefRowExists.value = Boolean(savedPref)
|
||||
viewCode.value = mergeDefaultViewCode(
|
||||
render.defaultViewCode,
|
||||
viewCodes.value = mergeDefaultViewCodes(
|
||||
render.defaultViewCodes,
|
||||
savedPref,
|
||||
render.views.map((view) => view.code),
|
||||
allowedViewCodesOf(render.views),
|
||||
)
|
||||
quickValues.value = defaultQuickValues(render.quickFilters)
|
||||
appliedQuick.value = { ...quickValues.value }
|
||||
@@ -542,15 +548,36 @@ function withTieBreaker(orderSql, field) {
|
||||
return sorted ? orderSql : `${orderSql}, ${field} ASC`
|
||||
}
|
||||
|
||||
/** 分组 → 该组可用页签编码集合(个人默认页签只能落在允许的集合里)。
|
||||
* 无原型对象:分组名由管理员填写,避免 '__proto__' 之类的键命中原型链 */
|
||||
function allowedViewCodesOf(viewList) {
|
||||
const map = Object.create(null)
|
||||
for (const view of viewList || []) {
|
||||
const group = String(view.group ?? '')
|
||||
if (!map[group]) map[group] = new Set()
|
||||
map[group].add(String(view.code))
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/** 页签选中集合(分组 → 编码)→ 各页签 AST:组间条件叠加,某组不限即不加该组条件 */
|
||||
function viewAstsOf(codes) {
|
||||
const asts = []
|
||||
for (const code of Object.values(codes || {})) {
|
||||
const view = views.value.find((item) => item.code === code) || null
|
||||
if (view) asts.push(buildViewAst(view, activeFieldById.value))
|
||||
}
|
||||
return asts
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定页签下的完整查询 AST:固定条件之外的四个来源按「且」合并 ——
|
||||
* 页签预设条件 / 关键词 / 快捷筛选 / 更多筛选。
|
||||
* 完整查询 AST:固定条件之外的四个来源按「且」合并 ——
|
||||
* 选中的页签条件 / 关键词 / 快捷筛选 / 更多筛选。
|
||||
* 页签条件与用户条件固定 AND,用户永远是在当前工作队列里继续收窄。
|
||||
*/
|
||||
function buildQueryAstOf(code) {
|
||||
const view = views.value.find((item) => item.code === code) || null
|
||||
function buildQueryAstOf(codes) {
|
||||
return combineAst(
|
||||
buildViewAst(view, activeFieldById.value),
|
||||
...viewAstsOf(codes),
|
||||
buildKeywordAst(keyword.value, keywordConfig.value?.items || []),
|
||||
buildQuickAst(quickItems.value, appliedQuick.value),
|
||||
buildFilterStateAst(appliedFilters.value, queryItemByField.value),
|
||||
@@ -559,7 +586,7 @@ function buildQueryAstOf(code) {
|
||||
|
||||
/** 当前生效的查询 AST(任何条件变化后重新计算,供 SQL 生成与条件回显共用) */
|
||||
function buildQueryAst() {
|
||||
return buildQueryAstOf(viewCode.value)
|
||||
return buildQueryAstOf(viewCodes.value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -580,6 +607,13 @@ function buildSearchCondition() {
|
||||
return conditionSqlOf(queryAst.value)
|
||||
}
|
||||
|
||||
/** 页签数量的条件:把目标页签当作它所在组的选中,其余组保持当前选中(「切过去会看到多少条」) */
|
||||
function tabCountConditionSql(code) {
|
||||
const view = views.value.find((item) => item.code === code) || null
|
||||
const group = String(view?.group ?? '')
|
||||
return conditionSqlOf(buildQueryAstOf({ ...viewCodes.value, [group]: code }))
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
if (!dataModule.value || !dataModule.value.b_view_table) return
|
||||
|
||||
@@ -664,14 +698,14 @@ async function initialize() {
|
||||
editConfigById.value = new Map()
|
||||
queryRender.value = {
|
||||
views: [],
|
||||
defaultViewCode: '',
|
||||
defaultViewCodes: {},
|
||||
keywordSearch: null,
|
||||
quickFilters: [],
|
||||
filterGroups: [],
|
||||
moreFilters: [],
|
||||
}
|
||||
viewCode.value = ''
|
||||
defaultViewCode.value = ''
|
||||
savedDefaultViewCode.value = ''
|
||||
viewCodes.value = {}
|
||||
defaultViewCodes.value = {}
|
||||
savedDefaultViewCodes.value = {}
|
||||
keyword.value = ''
|
||||
quickValues.value = {}
|
||||
appliedQuick.value = {}
|
||||
@@ -738,10 +772,10 @@ function searchKeyword(value) {
|
||||
return refreshQuery()
|
||||
}
|
||||
|
||||
/** 切换页签:页签条件与用户条件固定「且」,用户已填条件保留 */
|
||||
function changeView(code) {
|
||||
if (loading.value || !code || code === viewCode.value) return
|
||||
viewCode.value = code
|
||||
/** 切换页签:页签条件与用户条件固定「且」,用户已填条件保留(多组可同时生效) */
|
||||
function changeView(codes) {
|
||||
if (loading.value) return
|
||||
viewCodes.value = { ...(codes || {}) }
|
||||
return refreshQuery()
|
||||
}
|
||||
|
||||
@@ -802,23 +836,23 @@ function clearAllConditions() {
|
||||
|
||||
/* ── 个人层默认页签(s_user_module_pref, b_schema_type='query') ── */
|
||||
|
||||
/** 把当前页签设为「我的默认」;当它已是系统默认时,反而清掉个人覆盖(回落到系统默认) */
|
||||
/** 把当前选中的页签组合设为「我的默认」;与系统默认一致时清掉个人覆盖(回落系统默认) */
|
||||
async function setDefaultView() {
|
||||
const userId = currentUserId()
|
||||
const moduleId = String(dataModule.value?.b_id || '').trim()
|
||||
if (!userId || !moduleId || loading.value) return
|
||||
const delta = viewCodeDelta(viewCode.value, defaultViewCode.value)
|
||||
const delta = viewCodeDelta(viewCodes.value, defaultViewCodes.value)
|
||||
try {
|
||||
if (!delta) {
|
||||
if (prefRowExists.value) {
|
||||
await removeModulePref(userId, moduleId)
|
||||
prefRowExists.value = false
|
||||
}
|
||||
savedDefaultViewCode.value = ''
|
||||
savedDefaultViewCodes.value = {}
|
||||
} else {
|
||||
await saveModulePref(userId, moduleId, delta, prefRowExists.value)
|
||||
prefRowExists.value = true
|
||||
savedDefaultViewCode.value = delta.defaultViewCode
|
||||
savedDefaultViewCodes.value = { ...delta.defaultViewCodes }
|
||||
}
|
||||
Message.success(t('common.defaultViewSaved', '已设为我的默认页签'))
|
||||
} catch (error) {
|
||||
@@ -865,7 +899,7 @@ async function loadTabCounts(codes) {
|
||||
viewName: module.b_view_table,
|
||||
orderBy,
|
||||
searchColumns: [],
|
||||
searchCondition: conditionSqlOf(buildQueryAstOf(code)),
|
||||
searchCondition: tabCountConditionSql(code),
|
||||
pageNo: 1,
|
||||
pageSize: 1,
|
||||
})
|
||||
@@ -1528,20 +1562,23 @@ defineExpose({
|
||||
|
||||
<template v-else>
|
||||
<div class="module-main-column">
|
||||
<!-- 上:查询卡(工作队列页签 + 关键词 + 快捷筛选 + 条件回显),全部由模块查询配置驱动 -->
|
||||
<section v-if="views.length" class="fms-module-card query-card">
|
||||
<!-- 上:查询卡(工作队列页签 + 关键词 + 快捷筛选 + 条件回显),全部由模块查询配置驱动;
|
||||
只要任一入口有配置就渲染(模块可能只配了关键词或快捷筛选) -->
|
||||
<section
|
||||
v-if="views.length || keywordConfig || quickItems.length"
|
||||
class="fms-module-card query-card"
|
||||
>
|
||||
<FmsWorkQueueTabs
|
||||
:views="views"
|
||||
:active-code="viewCode"
|
||||
:active-codes="viewCodes"
|
||||
:counts="tabCounts"
|
||||
@update:active-code="changeView"
|
||||
@update:active-codes="changeView"
|
||||
@load-counts="requestTabCounts"
|
||||
/>
|
||||
<FmsKeywordSearch
|
||||
v-if="keywordConfig"
|
||||
v-model="keyword"
|
||||
:placeholder="keywordConfig.placeholder"
|
||||
:trigger="keywordConfig.trigger"
|
||||
:placeholder="keywordPlaceholder"
|
||||
@search="searchKeyword"
|
||||
/>
|
||||
<FmsQuickFilters
|
||||
@@ -1550,6 +1587,7 @@ defineExpose({
|
||||
:items="quickItems"
|
||||
:loading="loading"
|
||||
:advanced-count="appliedFilterCount"
|
||||
:advanced-enabled="moreFilters.length > 0"
|
||||
@search="search"
|
||||
@reset="resetSearch"
|
||||
@open-advanced="openFilters"
|
||||
@@ -1686,7 +1724,7 @@ defineExpose({
|
||||
<FmsFilterDrawer
|
||||
v-model="filterDraft"
|
||||
v-model:open="filterOpen"
|
||||
:groups="filterGroups"
|
||||
:items="moreFilters"
|
||||
@apply="applyFilters"
|
||||
/>
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ const props = defineProps({
|
||||
loading: { type: Boolean, default: false },
|
||||
// 已生效的更多筛选条件数(角标)
|
||||
advancedCount: { type: Number, default: 0 },
|
||||
// 是否显示「更多筛选」入口:模块没有可展开的字段时不显示(避免打开空抽屉)
|
||||
advancedEnabled: { type: Boolean, default: true },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'search', 'reset', 'open-advanced'])
|
||||
const { t } = useI18nStore()
|
||||
@@ -79,7 +81,12 @@ function submit() {
|
||||
<Button type="outline" :disabled="loading" @click="emit('reset')">
|
||||
{{ t('common.reset', '重置') }}
|
||||
</Button>
|
||||
<Button type="ghost" class="quick-more" @click="emit('open-advanced')">
|
||||
<Button
|
||||
v-if="advancedEnabled"
|
||||
type="ghost"
|
||||
class="quick-more"
|
||||
@click="emit('open-advanced')"
|
||||
>
|
||||
<Filter :size="14" />
|
||||
{{ t('common.moreFilters', '更多筛选') }}
|
||||
<span v-if="advancedCount" class="quick-more__count">{{ advancedCount }}</span>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 工作队列页签:预置查询视图(全部 / 待处理 / 有异常 …),进入页面即可用。
|
||||
* 页签编码、文案、顺序、默认项、是否显示数量、预设条件全部来自模块查询配置,
|
||||
* 页签编码、文案、顺序、分组、默认项、是否显示数量、预设条件(SQL)全部来自模块查询配置,
|
||||
* 本组件不含任何模块专用分支,也不自己请求数据。
|
||||
*
|
||||
* 数量由页面按需懒加载:首屏只取可见页签,超出部分收进「更多」,展开时才补取。
|
||||
* 分组语义:一个分组是一行页签,组内单选(再点一次已选中的页签即取消,回到该组「不限」),
|
||||
* 组间条件叠加(如「状态」选待处理 + 「审核状态」选已审核 = 两个条件取且)。
|
||||
*
|
||||
* 数量由页面按需懒加载:首屏只取每组可见页签,超出部分收进该组「更多」,展开时才补取。
|
||||
* 统计失败不阻塞列表 —— 数量为空即不显示徽标,页签仍可切换。
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
@@ -13,27 +16,66 @@ import { Dropdown } from '@/components/ui'
|
||||
import { useI18nStore } from '@/stores/i18n'
|
||||
|
||||
const props = defineProps({
|
||||
// 已排序的页签配置:[{ code, label, order, showCount, conditions }]
|
||||
// 已排序的页签配置:[{ code, label, group, order, showCount, conditionSql, conditions }]
|
||||
views: { type: Array, default: () => [] },
|
||||
activeCode: { type: String, default: '' },
|
||||
// 分组 → 当前选中的页签编码(缺省 / 不匹配表示该组不限)
|
||||
activeCodes: { type: Object, default: () => ({}) },
|
||||
// 页签编码 → 数量(缺失或非数字表示「暂无数量」)
|
||||
counts: { type: Object, default: () => ({}) },
|
||||
// 直接铺开的页签数量上限,其余进「更多」
|
||||
// 每组直接铺开的页签数量上限,其余进「更多」
|
||||
visibleLimit: { type: Number, default: 6 },
|
||||
})
|
||||
const emit = defineEmits(['update:activeCode', 'load-counts'])
|
||||
const emit = defineEmits(['update:activeCodes', 'load-counts'])
|
||||
const { t } = useI18nStore()
|
||||
|
||||
const overflowOpen = ref(false)
|
||||
/** 按分组切分页签:组的顺序即配置顺序(同组页签已按 order 排好) */
|
||||
const viewGroups = computed(() => {
|
||||
const groups = []
|
||||
const byName = new Map()
|
||||
for (const view of props.views || []) {
|
||||
const name = String(view.group ?? '')
|
||||
if (!byName.has(name)) {
|
||||
const entry = { name, tabs: [], visible: [], overflow: [] }
|
||||
byName.set(name, entry)
|
||||
groups.push(entry)
|
||||
}
|
||||
byName.get(name).tabs.push(view)
|
||||
}
|
||||
for (const entry of groups) {
|
||||
entry.visible = entry.tabs.slice(0, props.visibleLimit)
|
||||
entry.overflow = entry.tabs.slice(props.visibleLimit)
|
||||
}
|
||||
return groups
|
||||
})
|
||||
|
||||
const visibleViews = computed(() => props.views.slice(0, props.visibleLimit))
|
||||
const overflowViews = computed(() => props.views.slice(props.visibleLimit))
|
||||
const overflowItems = computed(() =>
|
||||
overflowViews.value.map((view) => ({ key: view.code, label: view.label })),
|
||||
)
|
||||
const isOverflowActive = computed(() =>
|
||||
overflowViews.value.some((view) => view.code === props.activeCode),
|
||||
)
|
||||
/** 只有一个默认组时不显示组标签,视觉上与单行页签一致 */
|
||||
const hasNamedGroup = computed(() => (props.views || []).some((view) => String(view.group ?? '')))
|
||||
|
||||
function isActive(view) {
|
||||
return String(props.activeCodes?.[String(view.group ?? '')] ?? '') === String(view.code)
|
||||
}
|
||||
|
||||
/** 组内单选:点未选中的页签即选中;点已选中的页签即取消(该组回到「不限」) */
|
||||
function select(view) {
|
||||
const group = String(view.group ?? '')
|
||||
const next = { ...(props.activeCodes || {}) }
|
||||
if (next[group] === view.code) delete next[group]
|
||||
else next[group] = view.code
|
||||
emit('update:activeCodes', next)
|
||||
}
|
||||
|
||||
function overflowItems(entry) {
|
||||
return entry.overflow.map((view) => ({ key: view.code, label: view.label }))
|
||||
}
|
||||
|
||||
function onOverflowClick(entry, { key }) {
|
||||
const view = entry.overflow.find((item) => item.code === key)
|
||||
if (view) select(view)
|
||||
}
|
||||
|
||||
function isOverflowActive(entry) {
|
||||
return entry.overflow.some((view) => isActive(view))
|
||||
}
|
||||
|
||||
/** 页签数量:配置未开启统计或还没拿到数据时为 null(不渲染徽标) */
|
||||
function countOf(view) {
|
||||
@@ -43,72 +85,87 @@ function countOf(view) {
|
||||
}
|
||||
|
||||
/** 只请求需要数量的页签,过滤失败/未开启的 */
|
||||
function countsOf(views) {
|
||||
return views.filter((view) => view.showCount).map((view) => view.code)
|
||||
function requestCounts(views) {
|
||||
const codes = (views || []).filter((view) => view.showCount).map((view) => view.code)
|
||||
if (codes.length) emit('load-counts', codes)
|
||||
}
|
||||
|
||||
function select(code) {
|
||||
if (!code || code === props.activeCode) return
|
||||
emit('update:activeCode', code)
|
||||
}
|
||||
|
||||
function onOverflowClick({ key }) {
|
||||
select(key)
|
||||
}
|
||||
|
||||
// 首屏只取可见页签的数量;页签配置变化(切换模块)时重新取
|
||||
// 首屏只取每组可见页签的数量;页签配置变化(切换模块)时重新取
|
||||
watch(
|
||||
visibleViews,
|
||||
(views) => {
|
||||
const codes = countsOf(views)
|
||||
if (codes.length) emit('load-counts', codes)
|
||||
},
|
||||
() => viewGroups.value.flatMap((entry) => entry.visible),
|
||||
(views) => requestCounts(views),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 「更多」展开时才补取其余页签的数量:不展开就不为看不见的页签发请求
|
||||
watch(overflowOpen, (open) => {
|
||||
if (!open) return
|
||||
const codes = countsOf(overflowViews.value)
|
||||
if (codes.length) emit('load-counts', codes)
|
||||
})
|
||||
// 某组「更多」展开时才补取该组其余页签的数量:不展开就不为看不见的页签发请求
|
||||
function onOverflowOpen(entry, open) {
|
||||
if (open) requestCounts(entry.overflow)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fms-work-queue-tabs">
|
||||
<button
|
||||
v-for="view in visibleViews"
|
||||
:key="view.code"
|
||||
type="button"
|
||||
class="work-queue-tab"
|
||||
:class="{ 'is-active': view.code === activeCode }"
|
||||
@click="select(view.code)"
|
||||
>
|
||||
{{ view.label }}
|
||||
<span v-if="countOf(view) != null" class="work-queue-tab__count">{{ countOf(view) }}</span>
|
||||
</button>
|
||||
<div v-for="entry in viewGroups" :key="entry.name" class="work-queue-group">
|
||||
<span v-if="hasNamedGroup" class="work-queue-group__label">{{ entry.name }}</span>
|
||||
<div class="work-queue-group__tabs">
|
||||
<button
|
||||
v-for="view in entry.visible"
|
||||
:key="view.code"
|
||||
type="button"
|
||||
class="work-queue-tab"
|
||||
:class="{ 'is-active': isActive(view) }"
|
||||
@click="select(view)"
|
||||
>
|
||||
{{ view.label }}
|
||||
<span v-if="countOf(view) != null" class="work-queue-tab__count">{{ countOf(view) }}</span>
|
||||
</button>
|
||||
|
||||
<Dropdown
|
||||
v-if="overflowViews.length"
|
||||
:items="overflowItems"
|
||||
placement="bottom-start"
|
||||
@click="onOverflowClick"
|
||||
@update:open="overflowOpen = $event"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="work-queue-tab work-queue-tab--more"
|
||||
:class="{ 'is-active': isOverflowActive }"
|
||||
>
|
||||
{{ t('common.more', '更多') }}
|
||||
<ChevronDown :size="14" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
v-if="entry.overflow.length"
|
||||
:items="overflowItems(entry)"
|
||||
placement="bottom-start"
|
||||
@click="onOverflowClick(entry, $event)"
|
||||
@update:open="onOverflowOpen(entry, $event)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="work-queue-tab work-queue-tab--more"
|
||||
:class="{ 'is-active': isOverflowActive(entry) }"
|
||||
>
|
||||
{{ t('common.more', '更多') }}
|
||||
<ChevronDown :size="14" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fms-work-queue-tabs {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.work-queue-group {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 组标签:只有存在具名分组时渲染,默认组渲染空标签占位,保证各行页签左边缘对齐 */
|
||||
.work-queue-group__label {
|
||||
flex: none;
|
||||
min-width: 32px;
|
||||
color: var(--fms-text-tertiary);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.work-queue-group__tabs {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 布局读写与两层继承合并(系统默认 → 个人)。
|
||||
// s_module_schema —— 系统默认。查询入口以 query JSON(页签 + 关键词 + 快捷筛选 +
|
||||
// 更多筛选分组 + 字段算子)为来源;列宽顺序以 view JSON(columns 节点)
|
||||
// s_module_schema —— 系统默认。查询入口以 query JSON(页签分组 + 关键词 + 快捷筛选 +
|
||||
// 字段查询元数据)为来源;列宽顺序以 view JSON(columns 节点)
|
||||
// 为来源。query 布局由模块管理页维护,「保存为通用」不算布局调整。
|
||||
// s_user_module_pref —— 个人覆盖(b_schema_type + b_schema_json),只存相对系统默认的增量。
|
||||
// 两类偏好(query 默认页签 / view 栏位布局)共用本文件的读写与增量计算,靠 schemaType 区分。
|
||||
@@ -23,25 +23,49 @@ export function currentUserId() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人默认页签 → 增量覆盖({ schemaVersion: 1, defaultViewCode })。
|
||||
* 与系统默认一致时不落库(返回 null):两层继承下「无覆盖」就是跟随系统默认。
|
||||
* 个人默认页签 → 增量覆盖({ schemaVersion: 2, defaultViewCodes: { [分组]: 页签编码 } })。
|
||||
* 只记与系统默认不同的分组;完全没有差异时不落库(返回 null):
|
||||
* 两层继承下「无覆盖」就是跟随系统默认。
|
||||
* @param currentCodes 当前选中的页签(分组 → 编码)
|
||||
* @param baseCodes 系统默认页签(分组 → 编码)
|
||||
*/
|
||||
export function viewCodeDelta(currentCode, baseCode) {
|
||||
const current = String(currentCode ?? '')
|
||||
if (!current || current === String(baseCode ?? '')) return null
|
||||
return { schemaVersion: 1, defaultViewCode: current }
|
||||
export function viewCodeDelta(currentCodes, baseCodes) {
|
||||
const delta = {}
|
||||
const groups = new Set([
|
||||
...Object.keys(baseCodes || {}),
|
||||
...Object.keys(currentCodes || {}),
|
||||
])
|
||||
for (const group of groups) {
|
||||
const current = String(currentCodes?.[group] ?? '')
|
||||
if (!current || current === String(baseCodes?.[group] ?? '')) continue
|
||||
delta[group] = current
|
||||
}
|
||||
return Object.keys(delta).length ? { schemaVersion: 2, defaultViewCodes: delta } : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人层默认页签叠加到系统默认之上。
|
||||
* 个人层默认页签叠加到系统默认之上,返回「分组 → 页签编码」。
|
||||
* 个人值必须是本模块当前真实可用的页签编码(页签被删除 / 无权限时自动回落系统默认),
|
||||
* 偏好只能挑页签,不能凭空造出一个页签。
|
||||
* 兼容 v1 个人层(只有一个 defaultViewCode):视作默认组('')的覆盖。
|
||||
* @param allowedCodesByGroup 分组 → 该组可用页签编码集合
|
||||
*/
|
||||
export function mergeDefaultViewCode(baseCode, pref, allowedCodes) {
|
||||
const base = String(baseCode ?? '')
|
||||
const saved = String(pref?.defaultViewCode ?? '')
|
||||
if (!saved) return base
|
||||
return new Set((allowedCodes || []).map(String)).has(saved) ? saved : base
|
||||
export function mergeDefaultViewCodes(baseCodes, pref, allowedCodesByGroup) {
|
||||
const merged = { ...(baseCodes || {}) }
|
||||
const saved = pref?.defaultViewCodes && typeof pref.defaultViewCodes === 'object'
|
||||
? pref.defaultViewCodes
|
||||
: pref?.defaultViewCode
|
||||
? { '': String(pref.defaultViewCode) }
|
||||
: null
|
||||
if (!saved) return merged
|
||||
for (const [group, code] of Object.entries(saved)) {
|
||||
const allowed =
|
||||
allowedCodesByGroup instanceof Map
|
||||
? allowedCodesByGroup.get(group)
|
||||
: allowedCodesByGroup?.[group]
|
||||
if (allowed?.has?.(String(code))) merged[group] = String(code)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/* ================= 栏位布局(view)个人层 =================
|
||||
|
||||
Reference in new issue
Block a user