20260831223203
This commit is contained in:
1 parent
4d46ce6c53
commit
3bbcafc5be
91 files changed
+20921
-584
No files matched your search
@@ -45,7 +45,7 @@ function actionButton(action, row, triggerAction, className = 'fms-action-btn')
|
||||
// 确认弹窗:状态化渲染(替代 antdv 的 Modal.confirm 命令式调用),
|
||||
// 由内部 Modal 承载;okType=danger(默认)时用 danger 按钮定制 footer。
|
||||
function createConfirmDialog(state, close) {
|
||||
const { action, row } = state
|
||||
const { action, row } = state.value
|
||||
const config = typeof action.confirm === 'string' ? { title: action.confirm } : action.confirm || {}
|
||||
const title = config.title || '确认操作'
|
||||
const description = config.description ?? config.content ?? '确定要执行此操作吗?'
|
||||
@@ -132,6 +132,9 @@ function createActionsCell(actions) {
|
||||
.filter((action) => isActionVisible(action, row))
|
||||
.map((action) => actionButton(action, row, triggerAction))
|
||||
const visibleMoreItems = (moreAction?.items || []).filter((item) => isActionVisible(item, row))
|
||||
// 确认弹窗必须先于「无更多项」的提前 return 渲染——否则仅内联操作(如删除)点击后
|
||||
// confirmState 已设置但弹窗永远不会出现(表现为「删除按钮没有反应」)。
|
||||
if (confirmState.value) children.push(createConfirmDialog(confirmState, closeConfirm))
|
||||
if (!visibleMoreItems.length) return h('span', { class: 'fms-actions-cell' }, children)
|
||||
|
||||
const menuItems = visibleMoreItems.map((item, index) => ({
|
||||
@@ -200,7 +203,6 @@ function createActionsCell(actions) {
|
||||
)
|
||||
}
|
||||
|
||||
if (confirmState.value) children.push(createConfirmDialog(confirmState, closeConfirm))
|
||||
return h('span', { class: 'fms-actions-cell' }, children)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,20 +149,16 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function notifyUpdate() {
|
||||
if (localRow.value) emit('update', [{ ...localRow.value }])
|
||||
}
|
||||
|
||||
function toggleEnabled(checked) {
|
||||
if (!localRow.value) return
|
||||
localRow.value.b_canuse = checked ? 1 : 0
|
||||
notifyUpdate()
|
||||
if (localRow.value) emit('update', [{ ...localRow.value }])
|
||||
}
|
||||
|
||||
function setRowValue(field, value) {
|
||||
if (!localRow.value) return
|
||||
localRow.value[field] = value
|
||||
notifyUpdate()
|
||||
if (localRow.value) emit('update', [{ ...localRow.value }])
|
||||
}
|
||||
|
||||
function pad(value) {
|
||||
|
||||
@@ -77,14 +77,10 @@ function validate() {
|
||||
|
||||
defineExpose({ validate })
|
||||
|
||||
function emitUpdate() {
|
||||
if (formData.value) emit('update', formData.value)
|
||||
}
|
||||
|
||||
function setField(field, value) {
|
||||
if (!formData.value) return
|
||||
formData.value[field] = value === '' ? null : value
|
||||
emitUpdate()
|
||||
emit('update', formData.value)
|
||||
}
|
||||
|
||||
// 编码变化时始终以完整编码重新派生多语言键
|
||||
@@ -92,7 +88,7 @@ function onCodeInput(v) {
|
||||
setField('b_code', v)
|
||||
if (formData.value) {
|
||||
formData.value.b_i18n = buildModuleI18nKey(formData.value.b_module_type, v)
|
||||
emitUpdate()
|
||||
emit('update', formData.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +108,7 @@ function onModuleTypeChange(v) {
|
||||
formData.value.b_delete_policy = null
|
||||
}
|
||||
formData.value.b_i18n = buildModuleI18nKey(v, formData.value.b_code)
|
||||
emitUpdate()
|
||||
emit('update', formData.value)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* 字段定义面板(s_field):可编辑表格维护模块字段。
|
||||
* i18n 键按 field.{moduleCode}.{fieldName} 自动同步。
|
||||
* 表单/列表/查询三列为「参与勾选」:在一行内表达该字段参与了哪些界面,
|
||||
* 勾选即改即生效(生成/隐藏默认配置行);布局细节仍在对应设计器调整。
|
||||
* 参与是派生态(配置行存在且行内开关为 1),物化为字段行的 __form/__list/__query
|
||||
* 勾选即改即生效(建行/删行);布局细节仍在对应设计器调整。
|
||||
* 参与是派生态(配置行存在 = 参与),物化为字段行的 __form/__list/__query
|
||||
* 虚拟字段交给 FmsTable 内置 checkbox 渲染(与「启用」列同款),change 按列路由
|
||||
* 回 toggle 写配置草稿。参与开关语义见本文件内「字段参与开关」一节。
|
||||
*/
|
||||
@@ -16,11 +16,13 @@ import { createListConfigRow, createQueryConfigRow } from './components/fieldDef
|
||||
|
||||
/* ================= 字段参与开关(原 fieldParticipation.js 内联) =================
|
||||
* 「字段是否参与 表单/列表/查询」的判定与切换。语义沿用 fieldAreaDefaults 的
|
||||
* 项目约定(行存在 = 已配置,行内开关 = 是否参与):
|
||||
* 项目约定(行存在 = 参与):
|
||||
* - 表单参与:editConfig 行存在且 b_visible=1;关 = 置 b_visible=0,不删行
|
||||
* (保留画布布局/栅格,与列表「删除列=隐藏」同一哲学)
|
||||
* - 列表参与:listConfig 行存在且 b_visible=1(行由加载自动补全,通常恒存在)
|
||||
* - 查询参与:queryConfig 行存在且 b_canuse=1(行不自动生成,开 = 需要时按默认生成)
|
||||
* (保留画布布局/栅格)
|
||||
* - 列表参与:listConfig 行存在即参与(行由用户手动管理,加载时不自动补全,
|
||||
* 开 = 建行、关 = 删行,与查询面板的行存在语义一致)
|
||||
* - 查询参与:queryConfig 行存在即参与(无 b_canuse 开关,开 = 建行、关 = 删行,
|
||||
* 与查询面板「左字段库 + 右表格」的行存在语义一致)
|
||||
*
|
||||
* 所有 toggle 返回新数组:目标行浅拷贝后修改,其余行保持原引用;
|
||||
* 行不存在且要「关」时无操作,原样返回(调用方可直接 emit)。
|
||||
@@ -51,16 +53,14 @@ function isFormOn(editConfig, field) {
|
||||
}
|
||||
|
||||
function isListOn(listConfig, field) {
|
||||
const row = findParticipationRow(listConfig, field.b_id)
|
||||
return !!row && Number(row.b_visible) === 1
|
||||
return !!findParticipationRow(listConfig, field.b_id)
|
||||
}
|
||||
|
||||
function isQueryOn(queryConfig, field) {
|
||||
const row = findParticipationRow(queryConfig, field.b_id)
|
||||
return !!row && Number(row.b_canuse) === 1
|
||||
return !!findParticipationRow(queryConfig, field.b_id)
|
||||
}
|
||||
|
||||
/** 行不存在时追加到末尾;forceOn 的行由工厂置参与标志为 1(显式意愿) */
|
||||
/** 行不存在时追加到末尾 */
|
||||
function appendRow(rows, row) {
|
||||
return [...(rows || []), row]
|
||||
}
|
||||
@@ -75,15 +75,17 @@ function toggleForm(editConfig, field, moduleId) {
|
||||
function toggleList(listConfig, field, moduleId) {
|
||||
const rows = listConfig || []
|
||||
const row = findParticipationRow(rows, field.b_id)
|
||||
if (row) return withRowToggled(rows, row, 'b_visible')
|
||||
return appendRow(rows, createListConfigRow(moduleId, field, rows, { forceOn: true }))
|
||||
// 行存在 = 参与:取消勾选即删行;不存在 = 未参与:勾选即建默认行
|
||||
if (row) return rows.filter((item) => item !== row)
|
||||
return appendRow(rows, createListConfigRow(moduleId, field, rows))
|
||||
}
|
||||
|
||||
function toggleQuery(queryConfig, field, moduleId) {
|
||||
const rows = queryConfig || []
|
||||
const row = findParticipationRow(rows, field.b_id)
|
||||
if (row) return withRowToggled(rows, row, 'b_canuse')
|
||||
return appendRow(rows, createQueryConfigRow(moduleId, field, rows, { forceOn: true }))
|
||||
// 行存在 = 参与:取消勾选即删行;不存在 = 未参与:勾选即建默认行
|
||||
if (row) return rows.filter((item) => item !== row)
|
||||
return appendRow(rows, createQueryConfigRow(moduleId, field, rows))
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
@@ -161,17 +163,6 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function emitUpdate() {
|
||||
// 剥离参与虚拟字段:__ 前缀仅供表格渲染,不进字段草稿与保存请求
|
||||
emit(
|
||||
'update',
|
||||
localFields.value.map((row) => {
|
||||
const { __form, __list, __query, ...field } = row
|
||||
return field
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function onCellChange(row, col) {
|
||||
// 参与勾选:内置 checkbox 已把值写在字段行虚拟字段上(展示用途),
|
||||
// 真实状态经 toggle 写回对应配置草稿,不走字段 i18n 同步
|
||||
@@ -188,7 +179,14 @@ function onCellChange(row, col) {
|
||||
return
|
||||
}
|
||||
syncModuleItemI18nKeys(localFields.value, 'field', props.moduleCode, 'b_field')
|
||||
emitUpdate()
|
||||
// 剥离参与虚拟字段:__ 前缀仅供表格渲染,不进字段草稿与保存请求
|
||||
emit(
|
||||
'update',
|
||||
localFields.value.map((row) => {
|
||||
const { __form, __list, __query, ...field } = row
|
||||
return field
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const columns = shallowRef(buildColumns())
|
||||
|
||||
@@ -1,233 +1,101 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 多语言面板(s_i18n):透视展示——每行一个 b_key,每种语言一列。
|
||||
* 受管键(模块/字段/权限的 b_i18n)自动维护:默认语言为空时自动跟随当前名称。
|
||||
* 语言列表使用前端常量(DEFAULT_LANGUAGES),后续可切换语言类型表。
|
||||
* 数据由 index.vue 维护(加载、受管键同步、保存 diff),
|
||||
* 本面板只负责编辑交互(新增/删除/编辑单元格)并 emit 更新。
|
||||
*/
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { Message, Button, Input } from '@/components/ui'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { Plus, Trash2 } from '@lucide/vue'
|
||||
import { Button, Input } from '@/components/ui'
|
||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||
import { createTempId } from '@/utils/tempId'
|
||||
import { cloneData } from '@/utils/dataChanges'
|
||||
import { consolidatePivotRows, getI18nNamespace } from '@/utils/i18n'
|
||||
import { loadDataApi } from '@/services/api'
|
||||
// 前端默认语言列表(默认语言排第一)。
|
||||
// 后续若引入语言类型表(s_i18n_type)再改为接口加载。
|
||||
const DEFAULT_LANGUAGES = [
|
||||
{ code: 'zh-CN', name: '简体中文', default: true },
|
||||
{ code: 'en-US', name: 'English', default: false },
|
||||
]
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
/** 受管键项:[{ key, sourceKey, defaultValue, id }] */
|
||||
i18nItems: { type: Array, default: () => [] },
|
||||
/** 透视行数组(由 index.vue 维护) */
|
||||
rows: { type: Array, default: () => [] },
|
||||
/** 语言列表(index.vue 传入,保存 diff 使用同一份) */
|
||||
languages: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const langOptions = DEFAULT_LANGUAGES
|
||||
const originalsFlat = ref([])
|
||||
const pivotRows = ref([])
|
||||
const isLoading = ref(false)
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const langOptions = props.languages
|
||||
const localRows = ref([])
|
||||
const searchKey = ref('')
|
||||
const activeKey = ref(null)
|
||||
const tableRef = ref(null)
|
||||
const selectedRowKeys = ref([])
|
||||
|
||||
const managedItems = computed(() => props.i18nItems.filter((item) => item && item.managed))
|
||||
|
||||
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)
|
||||
})
|
||||
watch(
|
||||
() => props.rows,
|
||||
(val) => {
|
||||
if (val === localRows.value) return
|
||||
localRows.value = val.map((row) => ({ ...row }))
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function isManagedRow(row) {
|
||||
return Boolean(row && row._managedId)
|
||||
}
|
||||
|
||||
// 判断受管行是否为"无内容":非默认语言全空、默认语言只有自动跟随的名称
|
||||
// (来自数据库 _localeIds 或用户改过任何值都算有内容)。编码输入的中间态
|
||||
// 键(module.b → module.b_ → module.b_user)只会产生这种空行。
|
||||
function isEmptyManagedRow(row, defaultLocale) {
|
||||
if (row._localeIds && Object.keys(row._localeIds).length) return false
|
||||
for (const lang of langOptions) {
|
||||
const value = row[lang.code]
|
||||
if (value == null || value === '') continue
|
||||
if (lang.code === defaultLocale && String(value) === String(row._defaultValue ?? '')) continue
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 保证模块、字段、权限的受管键始终存在。默认语言为空或仍为上一次默认名称时,
|
||||
// 自动跟随当前名称;人工修改后的翻译不再被名称变化覆盖。
|
||||
function reconcileManagedRows(itemsOverride) {
|
||||
const items = itemsOverride || managedItems.value
|
||||
const rows = [...pivotRows.value]
|
||||
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[0]?.code || ''
|
||||
|
||||
for (const item of items) {
|
||||
const managedId = String(item.id || item.key)
|
||||
currentManagedIds.add(managedId)
|
||||
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) {
|
||||
// _syncInjected 标记由 reconcile 自动创建(非用户手输),键失效且无内容时可整行丢弃
|
||||
row = { _id: createTempId(), b_key: item.key, _localeIds: {}, _syncInjected: true }
|
||||
rows.push(row)
|
||||
}
|
||||
const previousDefault = row._defaultValue
|
||||
const nextDefault = item.defaultValue == null ? '' : String(item.defaultValue)
|
||||
if (
|
||||
defaultLocale &&
|
||||
(row[defaultLocale] == null ||
|
||||
row[defaultLocale] === '' ||
|
||||
row[defaultLocale] === previousDefault)
|
||||
) {
|
||||
row[defaultLocale] = nextDefault
|
||||
}
|
||||
row.b_key = String(item.key)
|
||||
row._managedId = managedId
|
||||
row._defaultValue = nextDefault
|
||||
row._sortType = String(item.key).split('.', 1)[0]
|
||||
}
|
||||
|
||||
// 键已失效的受管行:自动创建且无内容的直接丢弃(如编码输入中间态产生的空行,
|
||||
// 不丢弃会残留 module.b / module.b_ / module.b_user 一串);有翻译或来自数据库的
|
||||
// 解除保护保留,供管理员自行处理
|
||||
const keptRows = []
|
||||
for (const row of rows) {
|
||||
if (row._managedId && !currentManagedIds.has(String(row._managedId))) {
|
||||
if (row._syncInjected && isEmptyManagedRow(row, defaultLocale)) continue
|
||||
delete row._managedId
|
||||
delete row._defaultValue
|
||||
}
|
||||
keptRows.push(row)
|
||||
}
|
||||
pivotRows.value = consolidatePivotRows(keptRows)
|
||||
}
|
||||
|
||||
// 将扁平记录按 key 聚合成一行(每种语言一列)
|
||||
function buildPivot(flat) {
|
||||
const byKey = new Map()
|
||||
for (const r of flat) {
|
||||
if (!byKey.has(r.b_key)) byKey.set(r.b_key, [])
|
||||
byKey.get(r.b_key).push(r)
|
||||
}
|
||||
const rows = []
|
||||
for (const [key, recs] of byKey) {
|
||||
const row = { _id: createTempId(), b_key: key, _localeIds: {} }
|
||||
for (const r of recs) {
|
||||
row[r.b_locale] = r.b_value
|
||||
row._localeIds[r.b_locale] = r.b_id
|
||||
}
|
||||
rows.push(row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
async function loadI18n() {
|
||||
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 res = where ? await loadDataApi('s_i18n', where, 'b_key ASC, b_locale ASC') : { data: [] }
|
||||
if (String(props.moduleId) !== moduleId) return
|
||||
const flat = (res.data || []).map((row) => ({
|
||||
b_id: row.b_id,
|
||||
b_key: row.b_key,
|
||||
b_locale: row.b_locale,
|
||||
b_value: row.b_value,
|
||||
}))
|
||||
originalsFlat.value = cloneData(flat)
|
||||
pivotRows.value = buildPivot(flat)
|
||||
reconcileManagedRows()
|
||||
} catch (e) {
|
||||
Message.error('加载多语言失败:' + (e?.message || e))
|
||||
} finally {
|
||||
if (String(props.moduleId) === moduleId) isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.moduleId,
|
||||
() => {
|
||||
loadI18n()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 字段/权限草稿变化(含同步字段、改名、编码输入)时实时补齐受管行:finalI18nItems
|
||||
// 每次重算都是新数组,引用变化即触发。防抖 300ms——编码连续输入时合并中间态,
|
||||
// 只 reconcile 最终键。加载中不做(loadI18n 完成后会自行 reconcile,避免竞态)。
|
||||
let reconcileTimer = null
|
||||
watch(
|
||||
() => props.i18nItems,
|
||||
() => {
|
||||
if (isLoading.value) return
|
||||
clearTimeout(reconcileTimer)
|
||||
reconcileTimer = setTimeout(() => reconcileManagedRows(), 300)
|
||||
},
|
||||
)
|
||||
onBeforeUnmount(() => clearTimeout(reconcileTimer))
|
||||
|
||||
function onCellChange() {
|
||||
reconcileManagedRows()
|
||||
}
|
||||
|
||||
async function addRow() {
|
||||
if (props.loading || isLoading.value) return
|
||||
if (props.loading) return
|
||||
const newId = createTempId()
|
||||
// stk-table 对同一引用数组的 push 不敏感,必须换新引用才会渲染新行
|
||||
pivotRows.value = [...pivotRows.value, { _id: newId, b_key: '', _localeIds: {} }]
|
||||
localRows.value = [...localRows.value, { _id: newId, b_key: '', _localeIds: {} }]
|
||||
activeKey.value = String(newId)
|
||||
emit('update', localRows.value)
|
||||
await nextTick()
|
||||
tableRef.value?.editRow?.(String(newId), 'b_key')
|
||||
}
|
||||
|
||||
function removeRow(row) {
|
||||
if (isManagedRow(row)) return
|
||||
pivotRows.value = pivotRows.value.filter((r) => r !== row)
|
||||
localRows.value = localRows.value.filter((r) => r !== row)
|
||||
selectedRowKeys.value = selectedRowKeys.value.filter((x) => x !== String(row._id))
|
||||
emit('update', localRows.value)
|
||||
}
|
||||
|
||||
function removeByManagedIds(ids) {
|
||||
const set = new Set(ids.map(String))
|
||||
pivotRows.value = pivotRows.value.filter((r) => !set.has(String(r._managedId)))
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
getCheckboxProps: (row) => ({ disabled: isManagedRow(row) }),
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
},
|
||||
}))
|
||||
|
||||
function batchDelete() {
|
||||
if (props.loading) return
|
||||
const keys = selectedRowKeys.value
|
||||
if (!keys.length) return
|
||||
const delSet = new Set(keys)
|
||||
const deletable = localRows.value.filter((r) => delSet.has(String(r._id)) && !isManagedRow(r))
|
||||
if (!deletable.length) return
|
||||
const delIds = new Set(deletable.map((r) => String(r._id)))
|
||||
localRows.value = localRows.value.filter((r) => !delIds.has(String(r._id)))
|
||||
selectedRowKeys.value = []
|
||||
if (activeKey.value && delIds.has(String(activeKey.value))) activeKey.value = null
|
||||
emit('update', localRows.value)
|
||||
}
|
||||
|
||||
function refreshKey(key) {
|
||||
if (!key) return
|
||||
loadI18n()
|
||||
function onCellChange() {
|
||||
emit('update', localRows.value)
|
||||
}
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const kw = String(searchKey.value || '').trim().toLowerCase()
|
||||
if (!kw) return pivotRows.value
|
||||
return pivotRows.value.filter((row) => String(row.b_key || '').toLowerCase().includes(kw))
|
||||
if (!kw) return localRows.value
|
||||
return localRows.value.filter((row) => String(row.b_key || '').toLowerCase().includes(kw))
|
||||
})
|
||||
|
||||
// 动态列:key 列 + 每种语言一列 + 操作列
|
||||
const columns = computed(() => {
|
||||
const cols = [
|
||||
{
|
||||
dataIndex: 'b_key',
|
||||
title: '多语言键',
|
||||
width: 260,
|
||||
// 不能带 customCell:resolveCustomCell 会让 customCell 优先而忽略 editor,导致新增行无法进入输入框
|
||||
editor: { type: 'input', disabled: (row) => isManagedRow(row) },
|
||||
},
|
||||
]
|
||||
@@ -248,111 +116,12 @@ const columns = computed(() => {
|
||||
type: 'delete',
|
||||
label: '删除',
|
||||
disabled: (row) => isManagedRow(row),
|
||||
confirm: '确定删除该多语言键?',
|
||||
onClick: (row) => removeRow(row),
|
||||
},
|
||||
],
|
||||
})
|
||||
return cols
|
||||
})
|
||||
|
||||
// 供父组件保存时调用:按最终草稿态生成 s_i18n 请求
|
||||
function buildSaveRequest() {
|
||||
reconcileManagedRows()
|
||||
const inserts = []
|
||||
const updates = []
|
||||
const deletes = []
|
||||
// 模块命名空间:这些键属于当前模块元数据,落库时写入 b_module_id 便于按模块级联删除;
|
||||
// 其余(common.* 等通用文案)b_module_id 保持 NULL,不归属任何模块
|
||||
const MODULE_NAMESPACES = ['module', 'menu', 'field', 'power', 'group']
|
||||
function moduleIdOf(key) {
|
||||
const ns = getI18nNamespace(key)
|
||||
return MODULE_NAMESPACES.includes(ns) ? props.moduleId : null
|
||||
}
|
||||
const originals = new Map(
|
||||
originalsFlat.value.map((r) => [`${r.b_key}|${r.b_locale}`, r]),
|
||||
)
|
||||
const seen = new Set()
|
||||
for (const row of pivotRows.value) {
|
||||
const key = String(row.b_key || '')
|
||||
if (!key) continue
|
||||
for (const lang of langOptions) {
|
||||
const locale = lang.code
|
||||
const cellKey = `${key}|${locale}`
|
||||
seen.add(cellKey)
|
||||
const value = row[locale] == null ? '' : String(row[locale])
|
||||
const original = originals.get(cellKey)
|
||||
const localeId = row._localeIds?.[locale]
|
||||
if (localeId) {
|
||||
if (!original || String(original.b_value ?? '') !== value) {
|
||||
updates.push({
|
||||
b_id: localeId,
|
||||
b_key: key,
|
||||
b_locale: locale,
|
||||
b_value: value,
|
||||
b_module_id: moduleIdOf(key),
|
||||
b_canuse: 1,
|
||||
})
|
||||
}
|
||||
} else if (value !== '') {
|
||||
inserts.push({
|
||||
// b_id 为临时主键,由父组件分配真实主键后经 applyInsertedIds 回写;
|
||||
// _rowId / _locale 为回写标记,后端按表元数据过滤会忽略这两个字段
|
||||
b_id: createTempId(),
|
||||
_rowId: row._id,
|
||||
_locale: locale,
|
||||
b_key: key,
|
||||
b_locale: locale,
|
||||
b_value: value,
|
||||
b_module_id: moduleIdOf(key),
|
||||
b_canuse: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const r of originalsFlat.value) {
|
||||
const cellKey = `${r.b_key}|${r.b_locale}`
|
||||
if (!seen.has(cellKey)) deletes.push({ b_id: r.b_id })
|
||||
}
|
||||
if (!inserts.length && !updates.length && !deletes.length) return null
|
||||
return { table: 's_i18n', key_field: 'b_id', inserts, updates, deletes }
|
||||
}
|
||||
|
||||
function markSaved() {
|
||||
const flat = []
|
||||
for (const row of pivotRows.value) {
|
||||
const key = String(row.b_key || '')
|
||||
if (!key) continue
|
||||
for (const lang of langOptions) {
|
||||
const locale = lang.code
|
||||
const localeId = row._localeIds?.[locale]
|
||||
const value = row[locale] == null ? '' : String(row[locale])
|
||||
if (localeId) flat.push({ b_id: localeId, b_key: key, b_locale: locale, b_value: value })
|
||||
else if (value !== '') flat.push({ b_id: null, b_key: key, b_locale: locale, b_value: value })
|
||||
}
|
||||
}
|
||||
originalsFlat.value = flat
|
||||
}
|
||||
|
||||
// 父组件为 inserts 分配真实主键后调用,把 ID 回写到行内 _localeIds,
|
||||
// 使 markSaved 后的基线记录带真实 b_id,避免下次保存重复插入同一 (b_key, b_locale)。
|
||||
function applyInsertedIds(inserts) {
|
||||
for (const ins of inserts || []) {
|
||||
if (!ins._rowId || !ins._locale || ins.b_id == null) continue
|
||||
const row = pivotRows.value.find((r) => String(r._id) === String(ins._rowId))
|
||||
if (row) row._localeIds[ins._locale] = String(ins.b_id)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
buildSaveRequest,
|
||||
markSaved,
|
||||
refreshKey,
|
||||
removeByManagedIds,
|
||||
reconcileManagedRows,
|
||||
applyInsertedIds,
|
||||
loadI18n,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -361,15 +130,18 @@ defineExpose({
|
||||
<div class="mm-i18n__search">
|
||||
<Input v-model="searchKey" placeholder="按多语言键过滤" allow-clear />
|
||||
</div>
|
||||
<Button type="primary" :disabled="loading || isLoading" @click="addRow">新增多语言</Button>
|
||||
<Button type="outline" :disabled="loading || isLoading" @click="loadI18n">刷新</Button>
|
||||
<Button type="primary" :disabled="loading" @click="addRow"><Plus :size="14" /> 新增多语言</Button>
|
||||
<Button type="danger" :disabled="loading || !selectedRowKeys.length" @click="batchDelete">
|
||||
<Trash2 :size="14" /> 批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : '' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mm-panel__body">
|
||||
<FmsTable
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data-source="filteredRows"
|
||||
:editable="!loading && !isLoading"
|
||||
:editable="!loading"
|
||||
:row-selection="rowSelection"
|
||||
row-key="_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(_, row) => (activeKey = String(row._id))"
|
||||
@@ -404,5 +176,4 @@ defineExpose({
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
* 列表配置面板壳组件(s_field_view)。
|
||||
* 实际编辑器为共享组件 components/design-editor/TableDesignEditor.vue(mode="list"),
|
||||
* 本壳只做 props/emits 接线 + 行工厂注入(列表行默认属性来自业务层 fieldDefaults.js)。
|
||||
* 「删除列 = 隐藏」等列表侧差异由编辑器按 mode 处理。
|
||||
* 列表配置为手动管理(行存在即展示,删除即真删),由编辑器按 mode 处理。
|
||||
*/
|
||||
import TableDesignEditor from './components/design-editor/TableDesignEditor.vue'
|
||||
import { listRowDefaults } from './components/fieldDefaults'
|
||||
@@ -27,7 +27,7 @@ const createRow = (field, groupId) => ({
|
||||
b_group_id: groupId || null,
|
||||
b_i18n: null,
|
||||
b_xh: 0,
|
||||
...listRowDefaults(field), // b_visible / b_width / b_sortable
|
||||
...listRowDefaults(field), // b_width / b_sortable(b_visible 恒为 1,行存在即展示)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* i18n 键按 power.{moduleCode}.{powerCode} 自动同步。
|
||||
*/
|
||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import { Message, Modal, Button } from '@/components/ui'
|
||||
import { Plus, Trash2 } from '@lucide/vue'
|
||||
import { Message, Button } from '@/components/ui'
|
||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||
import { syncModuleItemI18nKeys } from '@/utils/i18n'
|
||||
import { createTempId } from '@/utils/tempId'
|
||||
@@ -40,10 +41,6 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function emitUpdate() {
|
||||
emit('update', localPowers.value)
|
||||
}
|
||||
|
||||
async function addRow() {
|
||||
if (props.loading) return
|
||||
const newId = createTempId()
|
||||
@@ -61,7 +58,7 @@ async function addRow() {
|
||||
}
|
||||
// stk-table 对同一引用数组的 push 不敏感(内部 shallowRef + 引用 watch),必须换新引用才会渲染新行
|
||||
localPowers.value = [...localPowers.value, newRow]
|
||||
emitUpdate()
|
||||
emit('update', localPowers.value)
|
||||
activeKey.value = String(newId)
|
||||
await nextTick()
|
||||
tableRef.value?.editRow?.(String(newId), 'b_code')
|
||||
@@ -71,7 +68,7 @@ function deleteRow(record) {
|
||||
if (props.loading) return
|
||||
localPowers.value = localPowers.value.filter((r) => r.b_id !== record.b_id)
|
||||
selectedRowKeys.value = selectedRowKeys.value.filter((x) => x !== String(record.b_id))
|
||||
emitUpdate()
|
||||
emit('update', localPowers.value)
|
||||
}
|
||||
|
||||
const selectedRowKeys = ref([])
|
||||
@@ -90,13 +87,13 @@ function batchDelete() {
|
||||
localPowers.value = localPowers.value.filter((r) => !delSet.has(String(r.b_id)))
|
||||
selectedRowKeys.value = []
|
||||
if (activeKey.value && delSet.has(activeKey.value)) activeKey.value = ''
|
||||
emitUpdate()
|
||||
emit('update', localPowers.value)
|
||||
Message.success(`已删除 ${keys.length} 条权限`)
|
||||
}
|
||||
|
||||
function onCellChange() {
|
||||
syncModuleItemI18nKeys(localPowers.value, props.i18nPrefix, props.moduleCode, 'b_code')
|
||||
emitUpdate()
|
||||
emit('update', localPowers.value)
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
@@ -119,7 +116,6 @@ const columns = shallowRef([
|
||||
{
|
||||
type: 'delete',
|
||||
label: '删除',
|
||||
confirm: '确定删除该权限点?',
|
||||
onClick: (row) => deleteRow(row),
|
||||
},
|
||||
],
|
||||
@@ -130,9 +126,9 @@ const columns = shallowRef([
|
||||
<template>
|
||||
<div class="mm-panel">
|
||||
<div class="mm-panel__toolbar">
|
||||
<Button type="primary" :disabled="loading" @click="addRow">新增权限</Button>
|
||||
<Button type="primary" :disabled="loading" @click="addRow"><Plus :size="14" /> 新增权限</Button>
|
||||
<Button type="danger" :disabled="loading || !selectedRowKeys.length" @click="batchDelete">
|
||||
批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : '' }}
|
||||
<Trash2 :size="14" /> 批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : '' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mm-panel__body">
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 查询配置面板(s_field_query):字段/查询控件/操作符/默认值/启用/排序,支持行拖拽与新增。
|
||||
* 选择字段时按字段控件类型智能默认查询组件与操作符(可再手动改)。
|
||||
* 参与语义:生成的条件行用「启用」开关表达是否参与运行时——停用行保留配置,
|
||||
* 勾选即恢复;删除仅用于清掉误加的行(补充缺失会按字段重新生成)。
|
||||
* 查询配置面板(s_field_query):左字段库 + 右查询条件表格。
|
||||
* 参与语义:行存在即参与(无单独「启用」开关)——字段库点击加入/定位、
|
||||
* 点击 ✓ 或删除行即移除;删错的行可从字段库随时加回(生成默认组件/操作符)。
|
||||
*
|
||||
* 查询配置不自动生成:同步字段时不会为任何字段生成查询行(列表/表单设计不同),
|
||||
* 查询条件完全由用户在字段库手动点击加入。
|
||||
*
|
||||
* 交互:
|
||||
* - 左字段库(复用 design-editor 的 FieldLibrary):未加入的字段点击即按字段
|
||||
* 控件类型生成默认查询行;已加入的显示 ✓,点击定位到表格对应行。
|
||||
* - 右表格:新增查询条件(手动选字段)、编辑 查询组件/操作符/默认值、
|
||||
* 行拖拽排序、删除行;选字段时自动重置组件/操作符为字段类型默认值。
|
||||
*/
|
||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import { Button } from '@/components/ui'
|
||||
import { Plus, Trash2 } from '@lucide/vue'
|
||||
import { Button, Message } from '@/components/ui'
|
||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||
import { createTempId } from '@/utils/tempId'
|
||||
import { queryRowDefaults } from './components/fieldDefaults'
|
||||
import FieldLibrary from './components/design-editor/common/FieldLibrary.vue'
|
||||
import { createQueryConfigRow, queryRowDefaults } from './components/fieldDefaults'
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
queryConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
/** 启用字段中尚未配置查询条件的数量(父级派生),>0 时显示补充入口 */
|
||||
missingCount: { type: Number, default: 0 },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'fill-missing'])
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
const COMPONENT_OPTIONS = [
|
||||
{ label: '输入框', value: 'input' },
|
||||
@@ -45,6 +53,7 @@ const OPERATOR_OPTIONS = [
|
||||
const localQuery = ref([])
|
||||
const activeKey = ref(null)
|
||||
const tableRef = ref(null)
|
||||
const selectedRowKeys = ref([])
|
||||
|
||||
watch(
|
||||
() => props.queryConfig,
|
||||
@@ -55,14 +64,10 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function emitUpdate() {
|
||||
emit('update', localQuery.value)
|
||||
}
|
||||
|
||||
function onRowOrderChange(rows) {
|
||||
const sorted = rows.map((row, index) => ({ ...row, b_xh: (index + 1) * 10 }))
|
||||
localQuery.value = sorted
|
||||
emitUpdate()
|
||||
emit('update', localQuery.value)
|
||||
}
|
||||
|
||||
/* ================= 字段引用 ================= */
|
||||
@@ -100,7 +105,7 @@ function onCellChange(row, col) {
|
||||
row.b_default_value = null
|
||||
}
|
||||
}
|
||||
emitUpdate()
|
||||
emit('update', localQuery.value)
|
||||
}
|
||||
|
||||
/* ================= 行增删 ================= */
|
||||
@@ -120,10 +125,9 @@ async function addRow() {
|
||||
b_operator: 'like',
|
||||
b_default_value: null,
|
||||
b_xh: (localQuery.value.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
},
|
||||
]
|
||||
emitUpdate()
|
||||
emit('update', localQuery.value)
|
||||
activeKey.value = String(newId)
|
||||
await nextTick()
|
||||
// 聚焦第一个可编辑列(字段),选定字段后再补组件/操作符
|
||||
@@ -133,7 +137,57 @@ async function addRow() {
|
||||
function removeRow(row) {
|
||||
localQuery.value = localQuery.value.filter((item) => item !== row)
|
||||
if (String(row.b_id) === String(activeKey.value)) activeKey.value = null
|
||||
emitUpdate()
|
||||
selectedRowKeys.value = selectedRowKeys.value.filter((x) => x !== String(row.b_id))
|
||||
emit('update', localQuery.value)
|
||||
}
|
||||
|
||||
/* ================= 批量删除 ================= */
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
},
|
||||
}))
|
||||
|
||||
function batchDelete() {
|
||||
if (props.loading) return
|
||||
const keys = selectedRowKeys.value
|
||||
if (!keys.length) return
|
||||
const delSet = new Set(keys)
|
||||
localQuery.value = localQuery.value.filter((r) => !delSet.has(String(r.b_id)))
|
||||
selectedRowKeys.value = []
|
||||
if (activeKey.value && delSet.has(activeKey.value)) activeKey.value = null
|
||||
emit('update', localQuery.value)
|
||||
Message.success(`已删除 ${keys.length} 条查询条件`)
|
||||
}
|
||||
|
||||
/* ================= 字段库交互 ================= */
|
||||
// 已加入的字段(供 FieldLibrary ✓ 标记与点击定位)
|
||||
function findRowByFieldId(fieldId) {
|
||||
return localQuery.value.find((row) => String(row.b_field_id) === String(fieldId))
|
||||
}
|
||||
|
||||
// 当前选中行对应的字段 id(高亮字段库)
|
||||
const selectedFieldId = computed(() => {
|
||||
const row = localQuery.value.find((item) => String(item.b_id) === String(activeKey.value))
|
||||
return row?.b_field_id != null ? String(row.b_field_id) : null
|
||||
})
|
||||
|
||||
// 字段库点击:已加入 → 定位表格行;未加入 → 生成默认查询行加入
|
||||
function onFieldSelect(field) {
|
||||
if (!field || props.loading) return
|
||||
const fieldId = String(field.b_id)
|
||||
const existing = findRowByFieldId(fieldId)
|
||||
if (existing) {
|
||||
activeKey.value = String(existing.b_id)
|
||||
nextTick(() => tableRef.value?.scrollToRow?.(String(existing.b_id)))
|
||||
return
|
||||
}
|
||||
const row = createQueryConfigRow(props.moduleId, field, localQuery.value)
|
||||
localQuery.value = [...localQuery.value, row]
|
||||
emit('update', localQuery.value)
|
||||
activeKey.value = String(row.b_id)
|
||||
nextTick(() => tableRef.value?.scrollToRow?.(String(row.b_id)))
|
||||
}
|
||||
|
||||
const columns = shallowRef([
|
||||
@@ -156,13 +210,6 @@ const columns = shallowRef([
|
||||
editor: { type: 'select', options: OPERATOR_OPTIONS },
|
||||
},
|
||||
{ dataIndex: 'b_default_value', title: '默认查询值', width: 150, editor: { type: 'input' } },
|
||||
{
|
||||
dataIndex: 'b_canuse',
|
||||
title: '启用',
|
||||
width: 70,
|
||||
align: 'center',
|
||||
editor: { type: 'checkbox' },
|
||||
},
|
||||
{ dataIndex: 'b_xh', title: '排序号', width: 90 },
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
@@ -172,7 +219,6 @@ const columns = shallowRef([
|
||||
{
|
||||
type: 'delete',
|
||||
label: '删除',
|
||||
confirm: '确定删除该查询条件?',
|
||||
onClick: (row) => removeRow(row),
|
||||
},
|
||||
],
|
||||
@@ -181,53 +227,72 @@ const columns = shallowRef([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mm-panel">
|
||||
<div class="mm-panel__toolbar">
|
||||
<Button type="primary" :disabled="loading" @click="addRow">新增查询条件</Button>
|
||||
<Button
|
||||
v-if="missingCount > 0"
|
||||
type="outline"
|
||||
:disabled="loading"
|
||||
title="为尚未配置查询条件的启用字段生成默认行"
|
||||
@click="emit('fill-missing')"
|
||||
>
|
||||
补充缺失字段 ({{ missingCount }})
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mm-panel__body">
|
||||
<FmsTable
|
||||
ref="tableRef"
|
||||
v-model:columns="columns"
|
||||
:data-source="localQuery"
|
||||
:editable="!loading"
|
||||
:row-draggable="!loading"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||
@change="onCellChange"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
<div class="mm-qdesign">
|
||||
<FieldLibrary
|
||||
:fields="fields"
|
||||
:rows="localQuery"
|
||||
:selected-field-id="selectedFieldId"
|
||||
:loading="loading"
|
||||
drag-mode="native"
|
||||
item-title="点击加入查询;已加入的点击定位;从右侧表格删除行即移除"
|
||||
added-title="已加入查询"
|
||||
@select="onFieldSelect"
|
||||
/>
|
||||
|
||||
<div class="mm-qdesign__main">
|
||||
<div class="mm-qdesign__toolbar">
|
||||
<Button type="primary" :disabled="loading" @click="addRow"><Plus :size="14" /> 新增查询条件</Button>
|
||||
<Button type="danger" :disabled="loading || !selectedRowKeys.length" @click="batchDelete">
|
||||
<Trash2 :size="14" /> 批量删除
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mm-qdesign__body">
|
||||
<FmsTable
|
||||
ref="tableRef"
|
||||
v-model:columns="columns"
|
||||
:data-source="localQuery"
|
||||
:editable="!loading"
|
||||
:row-draggable="!loading"
|
||||
:row-selection="rowSelection"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(_, row) => (activeKey = String(row.b_id))"
|
||||
@change="onCellChange"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mm-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
/* 双栏布局:左字段库(FieldLibrary 自带右侧分割线与内边距)+ 右主区 */
|
||||
.mm-qdesign {
|
||||
display: grid;
|
||||
grid-template-columns: 184px minmax(0, 1fr);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
background: var(--fms-card);
|
||||
}
|
||||
|
||||
.mm-panel__toolbar {
|
||||
.mm-qdesign__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.mm-qdesign__toolbar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
|
||||
.mm-panel__body {
|
||||
.mm-qdesign__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
+11
-14
@@ -200,15 +200,11 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function emitUpdate() {
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
/* ================= 数据视图:行排序 ================= */
|
||||
function onRowOrderChange(rows) {
|
||||
const sorted = rows.map((row, index) => ({ ...row, b_xh: (index + 1) * 10 }))
|
||||
localEdit.value = sorted
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
function normalizedRow(row) {
|
||||
@@ -221,7 +217,7 @@ function normalizedRow(row) {
|
||||
|
||||
function emitRows(rows) {
|
||||
localEdit.value = rows.map(normalizedRow)
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
/* ================= 公共查询表 ================= */
|
||||
@@ -553,7 +549,7 @@ function deleteGroupDirect(section) {
|
||||
for (const row of localEdit.value) {
|
||||
if (String(row.b_group_id || '') === gid) row.b_group_id = null
|
||||
}
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
// 列表配置中引用该分组的行清理 b_group_id(引用完整性,非可见性抄写)
|
||||
const listUpdated = (props.listConfig || []).map((r) =>
|
||||
String(r.b_group_id || '') === gid ? { ...r, b_group_id: null } : r,
|
||||
@@ -1020,7 +1016,7 @@ function moveSelectedRow(delta) {
|
||||
function toggleRowFlag(row, key) {
|
||||
if (!row || props.loading) return
|
||||
toggleFlag(row, key)
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
function onLabelChip(row, key) {
|
||||
@@ -1045,7 +1041,7 @@ function pastePropsToSelected() {
|
||||
const row = selectedRow.value
|
||||
if (!row || props.loading || !clipboardProps.value) return
|
||||
applyClipboardProps(row, clipboardProps.value.props)
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
/* 字段库拖拽(clone 到画布):布局冻结模式下画布侧不会收到 add 事件,
|
||||
@@ -1143,7 +1139,7 @@ function toggleInspector(key) {
|
||||
const row = selectedRow.value
|
||||
if (!row || props.loading) return
|
||||
toggleFlag(row, key)
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
function onColspanChange(value) {
|
||||
@@ -1165,7 +1161,7 @@ function onColspanChange(value) {
|
||||
sorted[i].b_colspan = Math.max(1, base + (k < rem ? 1 : 0))
|
||||
})
|
||||
}
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
// 数字输入框 change(失焦/回车)提交:与原值不同才真正写入
|
||||
@@ -1181,7 +1177,7 @@ function onHeightChange(value) {
|
||||
const row = selectedRow.value
|
||||
if (!row || !isTextarea(row) || props.loading) return
|
||||
row.b_height = Math.min(480, Math.max(64, Number(value) || 120))
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
/* 占位符:change(失焦/回车)提交,有变化才真正写入 */
|
||||
@@ -1191,7 +1187,7 @@ function onPlaceholderChange(value) {
|
||||
const next = String(value ?? '').trim()
|
||||
if ((row.b_placeholder || '') === next) return
|
||||
row.b_placeholder = next || null
|
||||
emitUpdate()
|
||||
emit('update', localEdit.value)
|
||||
}
|
||||
|
||||
function removeRow(row) {
|
||||
@@ -1700,7 +1696,7 @@ onBeforeUnmount(() => {
|
||||
:active-key="activeKey"
|
||||
@back="switchView('design')"
|
||||
@row-click="(row) => (activeKey = String(row.b_id))"
|
||||
@change="emitUpdate"
|
||||
@change="() => emit('update', localEdit.value)"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
|
||||
@@ -2811,6 +2807,7 @@ textarea.mm-fdesign__ctrl {
|
||||
}
|
||||
|
||||
.mm-fdesign__preview-empty {
|
||||
grid-column: 1 / -1;
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
color: var(--fms-text-secondary);
|
||||
|
||||
+15
-25
@@ -12,8 +12,8 @@
|
||||
* - 删除列 = 真删;分组可启停;剪贴板全量键集;数据视图含 必填/只读/禁用 列
|
||||
* mode = 'list'(列表配置,s_field_view 行):
|
||||
* - 头部无编辑方式切换器
|
||||
* - 删除列 = 隐藏(b_visible=0,预览置灰);无分组启停;剪贴板仅 visible/width;
|
||||
* 数据视图含 标题多语言 列
|
||||
* - 删除列 = 真删行(行存在即展示,与查询配置同语义);无分组启停;
|
||||
* 剪贴板仅 列宽;数据视图含 标题多语言 列
|
||||
*
|
||||
* 用法:
|
||||
* <TableDesignEditor
|
||||
@@ -103,8 +103,8 @@ const editing = useStructureEditing({
|
||||
applyGroups: (groups, removedGroupIds) => emit('updateGroups', groups, removedGroupIds),
|
||||
applyRows: (rows) => emit('update', rows),
|
||||
requestRename: (gid) => structureTreeRef.value?.startRename(gid),
|
||||
// 子表:全量键集;列表:仅 显示/列宽
|
||||
clipboardKeys: isSubtable.value ? undefined : ['b_visible', 'b_width'],
|
||||
// 子表:全量键集;列表:仅 列宽
|
||||
clipboardKeys: isSubtable.value ? undefined : ['b_width'],
|
||||
fieldNameOf: (row) => fieldName(row),
|
||||
getField: (fieldId) => fieldMap.value.get(fieldId),
|
||||
})
|
||||
@@ -143,33 +143,24 @@ const { visualColumns, bandRows, headerColumns, ungroupedCount } = useBandLayout
|
||||
|
||||
const groupCount = computed(() => (props.groups || []).length)
|
||||
|
||||
function emitUpdate() {
|
||||
emit('update', localRows.value)
|
||||
}
|
||||
|
||||
/* ================= 数据视图:行排序 ================= */
|
||||
function onRowOrderChange(rows) {
|
||||
localRows.value = renumberRows(rows)
|
||||
emitUpdate()
|
||||
emit('update', localRows.value)
|
||||
}
|
||||
|
||||
/* ================= 列删除语义 ================= */
|
||||
// list 模式:列自动包含全部启用字段,「删除列」= 隐藏(b_visible=0,预览置灰);
|
||||
// subtable 模式:真删行
|
||||
function deleteOrHideRow(row) {
|
||||
/* ================= 列删除 ================= */
|
||||
// list / subtable 统一真删行(列表不再「隐藏」语义,行存在即展示)
|
||||
function deleteRow(row) {
|
||||
if (!row || props.loading) return
|
||||
if (isSubtable.value) removeRow(row)
|
||||
else {
|
||||
row.b_visible = 0
|
||||
emitUpdate()
|
||||
}
|
||||
removeRow(row)
|
||||
}
|
||||
|
||||
function onNodeDelete({ key }) {
|
||||
if (props.loading || !key) return
|
||||
if (key.startsWith('c:')) {
|
||||
const row = editing.findRowByFieldId(key.slice(2))
|
||||
if (row) deleteOrHideRow(row)
|
||||
if (row) deleteRow(row)
|
||||
} else if (key.startsWith('g:') && key !== 'g:ungrouped') {
|
||||
const band = bandMeta.value.metaMap.get(key.slice(2))
|
||||
if (band) deleteBand(band)
|
||||
@@ -181,7 +172,7 @@ function onRemoveSelected() {
|
||||
deleteBand(selectedBand.value)
|
||||
return
|
||||
}
|
||||
deleteOrHideRow(selectedRow.value)
|
||||
deleteRow(selectedRow.value)
|
||||
}
|
||||
|
||||
/* ================= 结构树事件转发 ================= */
|
||||
@@ -211,7 +202,7 @@ function onStructureContextMenuClick({ item, node }) {
|
||||
} else if (item.key === 'paste') {
|
||||
selectRow(node.row)
|
||||
pastePropsToSelected()
|
||||
} else if (item.key === 'delete') deleteOrHideRow(node.row)
|
||||
} else if (item.key === 'delete') deleteRow(node.row)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +254,7 @@ function toggleInspector(key) {
|
||||
const row = selectedRow.value
|
||||
if (!row || props.loading) return
|
||||
toggleFlag(row, key)
|
||||
emitUpdate()
|
||||
emit('update', localRows.value)
|
||||
}
|
||||
|
||||
/* ================= 视图切换 ================= */
|
||||
@@ -303,7 +294,6 @@ const dataColumns = computed(() => {
|
||||
} else {
|
||||
cols.push(
|
||||
{ dataIndex: 'b_width', title: '列宽', width: 90, editor: { type: 'input', inputType: 'number' } },
|
||||
{ dataIndex: 'b_visible', title: '显示', width: 70, align: 'center', editor: { type: 'checkbox' } },
|
||||
{ dataIndex: 'b_i18n', title: '标题多语言', width: 220 },
|
||||
)
|
||||
}
|
||||
@@ -410,7 +400,7 @@ const treeFeatures = computed(() => ({
|
||||
:field-of="fieldOf"
|
||||
:group-id-of-row="groupIdOfRow"
|
||||
:commit-col-drag="commitColDrag"
|
||||
:on-commit="emitUpdate"
|
||||
:on-commit="() => emit('update', localRows.value)"
|
||||
:empty-text="isSubtable ? undefined : '暂无列配置'"
|
||||
/>
|
||||
|
||||
@@ -480,7 +470,7 @@ const treeFeatures = computed(() => ({
|
||||
:row-drag-handle="false"
|
||||
@back="switchView('design')"
|
||||
@row-click="onDataRowClick"
|
||||
@change="emitUpdate"
|
||||
@change="() => emit('update', localRows.value)"
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ const props = defineProps({
|
||||
/** 剪贴板是否有列属性(决定右键「粘贴」可用性) */
|
||||
pasteEnabled: { type: Boolean, default: false },
|
||||
/** 宿主能力开关(默认全开):groupEnable 分组启停、colClipboard 列属性复制粘贴、
|
||||
* colDelete 列删除——列表配置等宿主「行存在即已配置、不删行」时关闭 */
|
||||
* colDelete 列删除——列表配置等宿主需按 mode 选择关闭 */
|
||||
features: { type: Object, default: () => ({}) },
|
||||
})
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ const canMoveDown = computed(
|
||||
() => !isUngrouped.value && !ctx.loading.value && !isEditing.value && props.node.canDown !== false,
|
||||
)
|
||||
const canDelete = computed(() => !isUngrouped.value && !ctx.loading.value && !isEditing.value)
|
||||
// 列节点的删除按钮按宿主 features 整体隐藏(列表配置语义:行存在即已配置,不删行)
|
||||
// 列节点的删除按钮按宿主 features 整体隐藏(列表配置行存在即展示,列删除按钮由宿主导航区/操作列提供)
|
||||
const showDeleteBtn = computed(() => isGroup.value || ctx.features.value.colDelete)
|
||||
// 分组:新增子分组按钮(未分组容器 / 列无此操作)
|
||||
const canAddSub = computed(() => isGroup.value && !ctx.loading.value && !isEditing.value)
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const props = defineProps({
|
||||
widthOf: { type: Function, required: true },
|
||||
/** 状态开关 chips 配置([{flag,label,icon,iconOff?}]),缺省为子表编辑的四项 */
|
||||
statusFlags: { type: Array, default: null },
|
||||
/** 是否显示「删除列」按钮(列表配置不删行,false 隐藏) */
|
||||
/** 是否显示「删除列」按钮(缺省显示;按宿主需要隐藏) */
|
||||
showRowDelete: { type: Boolean, default: true },
|
||||
/** 是否显示分组「启用」开关(列表配置无分组启停,false 隐藏) */
|
||||
showBandEnable: { type: Boolean, default: true },
|
||||
|
||||
+2
@@ -376,6 +376,7 @@ function mockValue(col) {
|
||||
<style lang="scss" scoped>
|
||||
/* 预览区:最上面的实时表格,只拖列宽 */
|
||||
.mm-fdesign__preview {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
max-height: 46%;
|
||||
min-height: 0;
|
||||
@@ -643,5 +644,6 @@ function mockValue(col) {
|
||||
min-height: 80px;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 字段区域默认策略(业务层,非编辑器内核)。
|
||||
* 「字段定义 → 列表列 / 查询条件默认配置行」的规则集中于此,供
|
||||
* index.vue(同步字段、补充缺失)、查询面板(选定字段后的组件/操作符重置)
|
||||
* index.vue(同步字段)、查询面板(选定字段后的组件/操作符重置)
|
||||
* 与字段定义面板的参与开关共用。
|
||||
*
|
||||
* 编辑器内核(design-editor/common/useStructureEditing.js)不依赖本文件——
|
||||
@@ -9,11 +9,6 @@
|
||||
*/
|
||||
import { createTempId } from '@/utils/tempId'
|
||||
|
||||
/** 大文本字段:默认不进列表、默认不作为查询条件 */
|
||||
function isLargeText(field) {
|
||||
return field.b_type === 'textarea' || field.b_dbtype === 'text'
|
||||
}
|
||||
|
||||
/** 列表列默认宽度(按控件类型) */
|
||||
const LIST_WIDTH_DEFAULTS = {
|
||||
input: 120,
|
||||
@@ -26,10 +21,13 @@ const LIST_WIDTH_DEFAULTS = {
|
||||
select: 110,
|
||||
}
|
||||
|
||||
/** 字段 → 列表列默认属性(b_visible / b_width / b_sortable) */
|
||||
/**
|
||||
* 字段 → 列表列默认属性(b_visible: 1 / b_width / b_sortable)。
|
||||
* 列表配置为手动管理(行存在即展示),b_visible 恒为 1,不再作为显示/隐藏开关。
|
||||
*/
|
||||
export function listRowDefaults(field) {
|
||||
return {
|
||||
b_visible: isLargeText(field) ? 0 : 1,
|
||||
b_visible: 1,
|
||||
b_width: LIST_WIDTH_DEFAULTS[field.b_type] ?? 100,
|
||||
b_sortable: 0,
|
||||
}
|
||||
@@ -45,7 +43,7 @@ const QUERY_COMPONENT_DEFAULTS = {
|
||||
checkbox: { component: 'checkbox', operator: 'eq' },
|
||||
}
|
||||
|
||||
/** 字段 → 查询条件默认属性(b_component / b_operator / b_canuse) */
|
||||
/** 字段 → 查询条件默认属性(b_component / b_operator) */
|
||||
export function queryRowDefaults(field) {
|
||||
const component = QUERY_COMPONENT_DEFAULTS[field.b_type] || {
|
||||
component: 'input',
|
||||
@@ -54,14 +52,13 @@ export function queryRowDefaults(field) {
|
||||
return {
|
||||
b_component: component.component,
|
||||
b_operator: component.operator,
|
||||
b_canuse: isLargeText(field) ? 0 : 1,
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 默认配置行工厂 ----------
|
||||
* 与 index.vue 的默认配置补全、字段定义面板参与开关共用,
|
||||
* 保证「同步字段自动补全」与「手动打开参与开关」生成的行结构一致。
|
||||
* forceOn:显式意愿(手动开开关)时无视大文本默认不参与的策略,置 1。
|
||||
* 与字段定义面板的参与开关共用,
|
||||
* 保证「手动打开参与开关」生成的行结构一致。
|
||||
* 列表/查询配置均为手动管理(行存在即展示/参与),行由用户在字段库手动添加生成。
|
||||
*/
|
||||
|
||||
function nextXh(rows) {
|
||||
@@ -74,7 +71,7 @@ function nextXh(rows) {
|
||||
}
|
||||
|
||||
/** 字段 → 列表列配置行(s_field_view),追加到末尾时使用 */
|
||||
export function createListConfigRow(moduleId, field, rows, { forceOn = false } = {}) {
|
||||
export function createListConfigRow(moduleId, field, rows) {
|
||||
return {
|
||||
b_id: createTempId(),
|
||||
b_module_id: String(moduleId),
|
||||
@@ -83,12 +80,11 @@ export function createListConfigRow(moduleId, field, rows, { forceOn = false } =
|
||||
b_i18n: field.b_i18n || null,
|
||||
b_xh: nextXh(rows),
|
||||
...listRowDefaults(field),
|
||||
...(forceOn ? { b_visible: 1 } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/** 字段 → 查询条件配置行(s_field_query) */
|
||||
export function createQueryConfigRow(moduleId, field, rows, { forceOn = false } = {}) {
|
||||
export function createQueryConfigRow(moduleId, field, rows) {
|
||||
return {
|
||||
b_id: createTempId(),
|
||||
b_module_id: String(moduleId),
|
||||
@@ -98,6 +94,5 @@ export function createQueryConfigRow(moduleId, field, rows, { forceOn = false }
|
||||
b_default_value: null,
|
||||
b_xh: nextXh(rows),
|
||||
...queryRowDefaults(field),
|
||||
...(forceOn ? { b_canuse: 1 } : {}),
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* 模块树 / 草稿加载与脏检查 / 保存编排逻辑直接内联在下方;确认弹窗状态机仅本页使用。
|
||||
* 设计器共用逻辑在 components/useStructureEditing.js,本文件不包含。
|
||||
*/
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useHotkeys } from '@/composables/useHotkeys'
|
||||
import {
|
||||
Plus,
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
buildModuleI18nKey,
|
||||
buildModuleItemI18nKey,
|
||||
syncModuleItemI18nKeys,
|
||||
consolidatePivotRows,
|
||||
getI18nNamespace,
|
||||
} from '@/utils/i18n'
|
||||
import { createTempId } from '@/utils/tempId'
|
||||
import {
|
||||
@@ -52,7 +54,6 @@ import {
|
||||
tableChange,
|
||||
hasChanges,
|
||||
} from '@/utils/dataChanges'
|
||||
import { createListConfigRow, createQueryConfigRow } from './components/fieldDefaults'
|
||||
import UnsavedChangesModal from '@/components/UnsavedChangesModal.vue'
|
||||
import ModuleBasicPanel from './ModuleBasicPanel.vue'
|
||||
import ModuleFieldsPanel from './ModuleFieldsPanel.vue'
|
||||
@@ -80,7 +81,6 @@ const drafts = ref(null)
|
||||
const activeSection = ref('basic')
|
||||
const basicPanelRef = ref(null)
|
||||
const autoCodePanelRef = ref(null)
|
||||
const i18nPanelRef = ref(null)
|
||||
// 新建模块时跳过 watch(moduleSelectedId) 的自动加载;非响应式 holder,跨 composable 共享
|
||||
const skipModuleLoad = { value: false }
|
||||
|
||||
@@ -137,6 +137,175 @@ const finalI18nItems = computed(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
// 前端默认语言列表(默认语言排第一);与面板共用同一份,保证保存 diff 与展示一致
|
||||
const I18N_LANGUAGES = [
|
||||
{ code: 'zh-CN', name: '简体中文', default: true },
|
||||
{ code: 'en-US', name: 'English', default: false },
|
||||
]
|
||||
|
||||
/* ================= i18n 数据维护(多语言面板数据由 index.vue 持有) ================= */
|
||||
|
||||
// 扁平记录(b_key/b_locale/b_value)→ 透视行(每行一个 key,每种语言一列)
|
||||
function buildI18nPivot(flat) {
|
||||
const byKey = new Map()
|
||||
for (const r of flat) {
|
||||
if (!byKey.has(r.b_key)) byKey.set(r.b_key, [])
|
||||
byKey.get(r.b_key).push(r)
|
||||
}
|
||||
const rows = []
|
||||
for (const [key, recs] of byKey) {
|
||||
const row = { _id: createTempId(), b_key: key, _localeIds: {} }
|
||||
for (const r of recs) {
|
||||
row[r.b_locale] = r.b_value
|
||||
row._localeIds[r.b_locale] = r.b_id
|
||||
}
|
||||
rows.push(row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
function isI18nManaged(row) {
|
||||
return Boolean(row && row._managedId)
|
||||
}
|
||||
|
||||
function isEmptyI18nManaged(row, defaultLocale) {
|
||||
if (row._localeIds && Object.keys(row._localeIds).length) return false
|
||||
for (const lang of I18N_LANGUAGES) {
|
||||
const value = row[lang.code]
|
||||
if (value == null || value === '') continue
|
||||
if (lang.code === defaultLocale && String(value) === String(row._defaultValue ?? '')) continue
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 保证模块/字段/权限受管键始终存在:默认语言为空时自动跟随当前名称,人工翻译不被覆盖
|
||||
function reconcileI18nRows() {
|
||||
if (!drafts.value) return
|
||||
const items = finalI18nItems.value
|
||||
const rows = [...drafts.value.i18n]
|
||||
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 = I18N_LANGUAGES[0]?.code || ''
|
||||
|
||||
for (const item of items) {
|
||||
const managedId = String(item.id || item.key)
|
||||
currentManagedIds.add(managedId)
|
||||
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: {}, _syncInjected: true }
|
||||
rows.push(row)
|
||||
}
|
||||
const previousDefault = row._defaultValue
|
||||
const nextDefault = item.defaultValue == null ? '' : String(item.defaultValue)
|
||||
if (
|
||||
defaultLocale &&
|
||||
(row[defaultLocale] == null ||
|
||||
row[defaultLocale] === '' ||
|
||||
row[defaultLocale] === previousDefault)
|
||||
) {
|
||||
row[defaultLocale] = nextDefault
|
||||
}
|
||||
row.b_key = String(item.key)
|
||||
row._managedId = managedId
|
||||
row._defaultValue = nextDefault
|
||||
row._sortType = String(item.key).split('.', 1)[0]
|
||||
}
|
||||
|
||||
// 键已失效的受管行:自动创建且无内容的直接丢弃,有翻译或来自数据库的解除保护保留
|
||||
const keptRows = []
|
||||
for (const row of rows) {
|
||||
if (row._managedId && !currentManagedIds.has(String(row._managedId))) {
|
||||
if (row._syncInjected && isEmptyI18nManaged(row, defaultLocale)) continue
|
||||
delete row._managedId
|
||||
delete row._defaultValue
|
||||
}
|
||||
keptRows.push(row)
|
||||
}
|
||||
drafts.value.i18n = consolidatePivotRows(keptRows)
|
||||
}
|
||||
|
||||
// 保存:按最终草稿态生成 s_i18n 请求(与加载时的扁平基线 diff)
|
||||
function buildI18nSaveRequest() {
|
||||
reconcileI18nRows()
|
||||
const rows = drafts.value?.i18n || []
|
||||
const originals = new Map(
|
||||
(originals.value?.i18n || []).map((r) => [`${r.b_key}|${r.b_locale}`, r]),
|
||||
)
|
||||
const inserts = []
|
||||
const updates = []
|
||||
const deletes = []
|
||||
// 模块命名空间:这些键属于当前模块元数据,落库写入 b_module_id 便于级联删除;其余保持 NULL
|
||||
const MODULE_NAMESPACES = ['module', 'menu', 'field', 'power', 'group']
|
||||
function moduleIdOf(key) {
|
||||
return MODULE_NAMESPACES.includes(getI18nNamespace(key)) ? String(selectedModule.value?.b_id) : null
|
||||
}
|
||||
const seen = new Set()
|
||||
for (const row of rows) {
|
||||
const key = String(row.b_key || '')
|
||||
if (!key) continue
|
||||
for (const lang of I18N_LANGUAGES) {
|
||||
const locale = lang.code
|
||||
const cellKey = `${key}|${locale}`
|
||||
seen.add(cellKey)
|
||||
const value = row[locale] == null ? '' : String(row[locale])
|
||||
const original = originals.get(cellKey)
|
||||
const localeId = row._localeIds?.[locale]
|
||||
if (localeId) {
|
||||
if (!original || String(original.b_value ?? '') !== value) {
|
||||
updates.push({ b_id: localeId, b_key: key, b_locale: locale, b_value: value, b_module_id: moduleIdOf(key), b_canuse: 1 })
|
||||
}
|
||||
} else if (value !== '') {
|
||||
inserts.push({
|
||||
b_id: createTempId(), _rowId: row._id, _locale: locale,
|
||||
b_key: key, b_locale: locale, b_value: value, b_module_id: moduleIdOf(key), b_canuse: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const r of originals.value?.i18n || []) {
|
||||
const cellKey = `${r.b_key}|${r.b_locale}`
|
||||
if (!seen.has(cellKey)) deletes.push({ b_id: r.b_id })
|
||||
}
|
||||
if (!inserts.length && !updates.length && !deletes.length) return null
|
||||
return { table: 's_i18n', key_field: 'b_id', inserts, updates, deletes }
|
||||
}
|
||||
|
||||
// 为新增记录回写真实主键到透视行 _localeIds,避免二次保存重复插入
|
||||
function applyI18nInsertedIds(inserts) {
|
||||
for (const ins of inserts || []) {
|
||||
if (!ins._rowId || !ins._locale || ins.b_id == null) continue
|
||||
const row = (drafts.value?.i18n || []).find((r) => String(r._id) === String(ins._rowId))
|
||||
if (row) row._localeIds[ins._locale] = String(ins.b_id)
|
||||
}
|
||||
}
|
||||
|
||||
// 同步字段时删除失效受管键(_managedId 为 `field:${i18nKey}` 等,见 finalI18nItems)
|
||||
function removeI18nByManagedIds(ids) {
|
||||
if (!drafts.value) return
|
||||
const set = new Set(ids.map(String))
|
||||
drafts.value.i18n = (drafts.value.i18n || []).filter((r) => !set.has(String(r._managedId)))
|
||||
}
|
||||
|
||||
// 字段/权限草稿变化(改名、编码输入)时防抖补齐受管行;加载中不处理(load 完成后自行 reconcile)
|
||||
let i18nReconcileTimer = null
|
||||
watch(
|
||||
finalI18nItems,
|
||||
() => {
|
||||
if (configLoading.value || !drafts.value) return
|
||||
clearTimeout(i18nReconcileTimer)
|
||||
i18nReconcileTimer = setTimeout(() => reconcileI18nRows(), 300)
|
||||
},
|
||||
)
|
||||
onBeforeUnmount(() => clearTimeout(i18nReconcileTimer))
|
||||
|
||||
/* 右侧分组配置导航:概览 / 数据结构 / 界面配置 / 业务能力 / 高级设置。
|
||||
每个导航项唯一激活(activeSection)。 */
|
||||
const configGroups = computed(() => {
|
||||
@@ -159,14 +328,9 @@ const configGroups = computed(() => {
|
||||
if (isDataModule.value) {
|
||||
uiItems.push({ key: 'edit', label: '表单设计', icon: Pencil })
|
||||
}
|
||||
// 列表配置行在加载时自动补全全部启用字段,无「缺失」概念,不带徽标
|
||||
// 列表/查询配置行均为手动管理(行存在即展示/参与),无「缺失」概念,不带徽标
|
||||
uiItems.push({ key: 'list', label: '列表配置', icon: List })
|
||||
uiItems.push({
|
||||
key: 'query',
|
||||
label: '查询配置',
|
||||
icon: Search,
|
||||
badge: missingQueryCount.value ? `缺 ${missingQueryCount.value}` : '',
|
||||
})
|
||||
uiItems.push({ key: 'query', label: '查询配置', icon: Search })
|
||||
groups.push({ key: 'display', label: '界面配置', items: uiItems })
|
||||
|
||||
const capabilityItems = []
|
||||
@@ -297,20 +461,15 @@ async function saveModule() {
|
||||
}
|
||||
}
|
||||
|
||||
const i18nPanel = i18nPanelRef.value
|
||||
if (i18nPanel && typeof i18nPanel.reconcileManagedRows === 'function') {
|
||||
i18nPanel.reconcileManagedRows(finalI18nItems.value)
|
||||
}
|
||||
if (i18nPanel && typeof i18nPanel.buildSaveRequest === 'function') {
|
||||
const i18nReq = i18nPanel.buildSaveRequest()
|
||||
if (i18nReq) {
|
||||
// 为新增的多语言记录分配真实主键,并回写内部状态,避免二次保存重复插入
|
||||
if (i18nReq.inserts?.length) {
|
||||
await allocateTemporaryIds([i18nReq.inserts])
|
||||
i18nPanel.applyInsertedIds?.(i18nReq.inserts)
|
||||
}
|
||||
saveReqs.push(i18nReq)
|
||||
// i18n 数据独立保存于 s_i18n:先补齐受管键,再按基线 diff 生成请求
|
||||
const i18nReq = buildI18nSaveRequest()
|
||||
if (i18nReq) {
|
||||
// 为新增的多语言记录分配真实主键,并回写草稿,避免二次保存重复插入
|
||||
if (i18nReq.inserts?.length) {
|
||||
await allocateTemporaryIds([i18nReq.inserts])
|
||||
applyI18nInsertedIds(i18nReq.inserts)
|
||||
}
|
||||
saveReqs.push(i18nReq)
|
||||
}
|
||||
|
||||
if (saveReqs.length === 0) {
|
||||
@@ -328,8 +487,6 @@ async function saveModule() {
|
||||
const prevSection = activeSection.value
|
||||
await loadModuleConfig(moduleId)
|
||||
activeSection.value = prevSection
|
||||
// i18n 数据独立保存于 s_i18n,不随模块配置草稿重载,单独从后端刷新
|
||||
i18nPanelRef.value?.loadI18n?.()
|
||||
Message.success('保存成功')
|
||||
return true
|
||||
} catch (e) {
|
||||
@@ -711,12 +868,24 @@ async function loadModuleConfig(moduleId) {
|
||||
}
|
||||
originals.value = original
|
||||
drafts.value = cloneData(original)
|
||||
// 列表配置自动包含全部启用字段:缺失的按控件类型生成默认列。
|
||||
// 「删除列」语义为隐藏(b_visible=0)而非删行——行删了下次加载又会自动补回。
|
||||
const listGenerated = buildMissingDefaultConfigs(original.module, original.fields)
|
||||
if (listGenerated.listConfig.length) {
|
||||
drafts.value.listConfig = [...(drafts.value.listConfig || []), ...listGenerated.listConfig]
|
||||
// i18n 独立加载(s_i18n 不随模块配置草稿加载,按受管键查询)
|
||||
if (type === 'data' || type === 'query') {
|
||||
const keys = [...new Set(finalI18nItems.value.flatMap((it) => [it.key, it.sourceKey]).filter(Boolean).map(String))]
|
||||
const quotedKeys = keys.map((key) => "'" + key.replace(/'/g, "''") + "'")
|
||||
const where = keys.length ? 'b_key IN (' + quotedKeys.join(', ') + ')' : null
|
||||
const res = where ? await loadDataApi('s_i18n', where, 'b_key ASC, b_locale ASC') : { data: [] }
|
||||
if (String(moduleSelectedId.value) !== id) return
|
||||
const flat = (res.data || []).map((row) => ({
|
||||
b_id: row.b_id, b_key: row.b_key, b_locale: row.b_locale, b_value: row.b_value,
|
||||
}))
|
||||
originals.value.i18n = flat
|
||||
drafts.value.i18n = buildI18nPivot(flat)
|
||||
reconcileI18nRows()
|
||||
} else {
|
||||
originals.value.i18n = []
|
||||
drafts.value.i18n = []
|
||||
}
|
||||
// 列表配置不再自动补全:行存在即展示,删除即真删,由用户通过字段库手动管理(与查询配置一致)。
|
||||
loadedModuleId.value = id
|
||||
activeSection.value = 'basic'
|
||||
} catch (e) {
|
||||
@@ -843,44 +1012,10 @@ function onAutoCodeUpdate(rows) {
|
||||
}
|
||||
|
||||
/* ================= 模块:同步字段 ================= */
|
||||
// 已配置过对应配置行的字段 id(列表/编辑/查询),用于判断新增字段是否缺默认配置
|
||||
function configuredFieldIds(section) {
|
||||
const ids = new Set()
|
||||
for (const source of [originals.value, drafts.value]) {
|
||||
for (const row of source?.[section] || []) {
|
||||
if (row.b_field_id != null) ids.add(String(row.b_field_id))
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// 为尚未配置的字段生成列表/查询默认行(行结构统一走 useStructureEditing 的工厂)。
|
||||
// 默认不参与的字段(大文本等)同样生成行、开关置 0,保留配置空间;
|
||||
// 表单设计(editConfig)不自动生成:字段由用户在表单设计器中逐个添加,
|
||||
// 自动铺满会与设计器的画布/分组/栅格编辑冲突。
|
||||
function buildMissingDefaultConfigs(module, fields) {
|
||||
const moduleId = String(module.b_id)
|
||||
const listFieldIds = configuredFieldIds('listConfig')
|
||||
const queryFieldIds = configuredFieldIds('queryConfig')
|
||||
const generated = { listConfig: [], queryConfig: [] }
|
||||
let orderIndex = 0
|
||||
for (const field of fields) {
|
||||
if (Number(field.b_canuse) !== 1) continue
|
||||
orderIndex++
|
||||
const fieldId = String(field.b_id)
|
||||
const order = orderIndex * 10
|
||||
if (!listFieldIds.has(fieldId)) {
|
||||
generated.listConfig.push({ ...createListConfigRow(moduleId, field, []), b_xh: order })
|
||||
}
|
||||
if (!queryFieldIds.has(fieldId)) {
|
||||
generated.queryConfig.push({ ...createQueryConfigRow(moduleId, field, []), b_xh: order })
|
||||
}
|
||||
}
|
||||
return generated
|
||||
}
|
||||
|
||||
// 同步只合并字段定义与移除已消失字段的配置行/i18n,不自动生成任何配置行——
|
||||
// 列表/查询配置为手动管理(行存在即展示),表单设计由用户逐个添加。
|
||||
// 计算同步计划(纯计算,不改草稿):describe 结果与现有字段合并、
|
||||
// 找出视图已不存在的字段、为新字段生成列表/编辑/查询默认配置。
|
||||
// 找出视图已不存在的字段。
|
||||
function buildSyncPlan(module, columns) {
|
||||
const viewColumnSet = new Set(columns.map((col) => String(col.name || '').toLowerCase()))
|
||||
const existingMap = new Map()
|
||||
@@ -924,9 +1059,7 @@ function buildSyncPlan(module, columns) {
|
||||
for (const field of mergedFields) {
|
||||
field.b_i18n = buildModuleItemI18nKey('field', module.b_code, field.b_field)
|
||||
}
|
||||
const generated = buildMissingDefaultConfigs(module, mergedFields)
|
||||
const generatedCount = generated.listConfig.length + generated.queryConfig.length
|
||||
return { mergedFields, removedIds, removedI18nIds, generated, addedCount, generatedCount }
|
||||
return { mergedFields, removedIds, removedI18nIds, addedCount }
|
||||
}
|
||||
|
||||
function applySyncPlan(plan) {
|
||||
@@ -936,18 +1069,10 @@ function applySyncPlan(plan) {
|
||||
(row) => !plan.removedIds.has(String(row.b_field_id)),
|
||||
)
|
||||
}
|
||||
i18nPanelRef.value?.removeByManagedIds?.(Array.from(plan.removedI18nIds))
|
||||
}
|
||||
for (const section of ['listConfig', 'queryConfig']) {
|
||||
const rows = plan.generated[section]
|
||||
if (!rows.length) continue
|
||||
drafts.value[section] = [...(drafts.value[section] || []), ...rows]
|
||||
removeI18nByManagedIds(Array.from(plan.removedI18nIds))
|
||||
}
|
||||
drafts.value.fields = plan.mergedFields
|
||||
const parts = [`已同步 ${plan.mergedFields.length} 个字段`]
|
||||
if (plan.addedCount) parts.push(`新增 ${plan.addedCount} 个并补充 ${plan.generatedCount} 条默认配置`)
|
||||
if (plan.removedIds.size) parts.push(`移除 ${plan.removedIds.size} 个及其关联配置`)
|
||||
Message.success(parts.join(','))
|
||||
Message.success(`已成功同步 ${plan.mergedFields.length} 个字段`)
|
||||
}
|
||||
|
||||
async function syncFields() {
|
||||
@@ -973,43 +1098,6 @@ async function syncFields() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 欠配置统计与一键补充 ================= */
|
||||
// 启用字段中尚未出现在指定配置(listConfig/queryConfig)的数量
|
||||
function missingConfigCount(section) {
|
||||
return computed(() => {
|
||||
const fields = drafts.value?.fields || []
|
||||
if (!fields.length) return 0
|
||||
const configured = new Set()
|
||||
for (const row of drafts.value?.[section] || []) {
|
||||
if (row.b_field_id != null) configured.add(String(row.b_field_id))
|
||||
}
|
||||
let count = 0
|
||||
for (const field of fields) {
|
||||
if (Number(field.b_canuse) !== 1) continue
|
||||
if (!configured.has(String(field.b_id))) count++
|
||||
}
|
||||
return count
|
||||
})
|
||||
}
|
||||
|
||||
const missingQueryCount = missingConfigCount('queryConfig')
|
||||
|
||||
// 为缺失的启用字段补充默认配置行(复用同步字段的默认行生成逻辑)
|
||||
function fillMissingConfigs(sections) {
|
||||
const module = drafts.value?.module
|
||||
if (!module || busy.value) return
|
||||
const generated = buildMissingDefaultConfigs(module, drafts.value.fields)
|
||||
let total = 0
|
||||
for (const section of sections) {
|
||||
const rows = generated[section]
|
||||
if (!rows?.length) continue
|
||||
drafts.value[section] = [...(drafts.value[section] || []), ...rows]
|
||||
total += rows.length
|
||||
}
|
||||
if (total) Message.success(`已补充 ${total} 条默认配置`)
|
||||
else Message.info('没有需要补充的配置')
|
||||
}
|
||||
|
||||
/* ================= 快捷键 ================= */
|
||||
// 仅保留 Ctrl/Cmd + S 保存当前对象(通用封装见 @/composables/useHotkeys)
|
||||
useHotkeys([
|
||||
@@ -1221,9 +1309,7 @@ loadModuleTree()
|
||||
:query-config="drafts?.queryConfig"
|
||||
:fields="drafts?.fields"
|
||||
:loading="busy"
|
||||
:missing-count="missingQueryCount"
|
||||
@update="(v) => onModuleUpdate('queryConfig', v)"
|
||||
@fill-missing="() => fillMissingConfigs(['queryConfig'])"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1258,10 +1344,11 @@ loadModuleTree()
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleI18nPanel
|
||||
ref="i18nPanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:i18n-items="finalI18nItems"
|
||||
:rows="drafts?.i18n"
|
||||
:languages="I18N_LANGUAGES"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('i18n', v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi, beforeAll, beforeEach } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { registerFeature, useAreaSelection } from 'stk-table-vue'
|
||||
import ModuleQueryConfigPanel from '@/views/system/module-management/ModuleQueryConfigPanel.vue'
|
||||
import ModulePowersPanel from '@/views/system/module-management/ModulePowersPanel.vue'
|
||||
import ModuleI18nPanel from '@/views/system/module-management/ModuleI18nPanel.vue'
|
||||
|
||||
registerFeature(useAreaSelection)
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||
configurable: true,
|
||||
get() { return 5000 }
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock('@/services/api', () => ({
|
||||
loadDataApi: vi.fn(async () => ({ data: [] })),
|
||||
saveObjectApi: vi.fn(),
|
||||
nextIdApi: vi.fn(),
|
||||
describeApi: vi.fn(),
|
||||
}))
|
||||
|
||||
const fields = [
|
||||
{ b_id: '1', b_name: '姓名', b_field: 'name', b_type: 'input', b_canuse: 1 }
|
||||
]
|
||||
|
||||
describe('module-management 删除(单个无确认弹窗 + 批量)', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
|
||||
async function settle() {
|
||||
await nextTick()
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
}
|
||||
|
||||
async function clickDelete(wrapper) {
|
||||
const trs = wrapper.findAll('.stk-tbody-main tr')
|
||||
expect(trs.length).toBeGreaterThan(0)
|
||||
const delBtn = trs[0].find('button.fms-action-btn')
|
||||
expect(delBtn, '行内删除按钮应存在').toBeTruthy()
|
||||
await delBtn.trigger('click')
|
||||
await settle()
|
||||
}
|
||||
|
||||
async function checkNoModal() {
|
||||
expect(document.body.querySelector('.fms-modal'), '删除不应弹出确认弹窗').toBeNull()
|
||||
}
|
||||
|
||||
async function selectRowCheckbox(wrapper, index = 0) {
|
||||
const trs = wrapper.findAll('.stk-tbody-main tr')
|
||||
expect(trs.length).toBeGreaterThan(0)
|
||||
const checkbox = trs[index].find('input[type="checkbox"]')
|
||||
expect(checkbox, '行复选框应存在').toBeTruthy()
|
||||
await checkbox.setValue(true)
|
||||
await settle()
|
||||
}
|
||||
|
||||
async function clickToolbarBatch(wrapper) {
|
||||
const btn = wrapper.findAll('button').find((b) => b.text().includes('批量删除'))
|
||||
expect(btn, '批量删除按钮应存在').toBeTruthy()
|
||||
await btn.trigger('click')
|
||||
await settle()
|
||||
}
|
||||
|
||||
it('查询面板:单个删除不弹弹窗且行被移除', async () => {
|
||||
const wrapper = mount(ModuleQueryConfigPanel, {
|
||||
props: { moduleId: 'm1', queryConfig: [], fields, loading: false },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await settle()
|
||||
await wrapper.findAll('button').find((b) => b.text().includes('新增查询条件')).trigger('click')
|
||||
await settle()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length).toBe(1)
|
||||
await clickDelete(wrapper)
|
||||
await checkNoModal()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length, '单个删除后行应被移除').toBe(0)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('查询面板:批量删除选中多行', async () => {
|
||||
const wrapper = mount(ModuleQueryConfigPanel, {
|
||||
props: { moduleId: 'm1', queryConfig: [], fields, loading: false },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await settle()
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await wrapper.findAll('button').find((b) => b.text().includes('新增查询条件')).trigger('click')
|
||||
await settle()
|
||||
}
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length).toBe(2)
|
||||
await selectRowCheckbox(wrapper, 0)
|
||||
await selectRowCheckbox(wrapper, 1)
|
||||
await clickToolbarBatch(wrapper)
|
||||
await checkNoModal()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length, '批量删除后应无行').toBe(0)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('权限面板:单个删除不弹弹窗且行被移除', async () => {
|
||||
const wrapper = mount(ModulePowersPanel, {
|
||||
props: { moduleId: 'm1', moduleCode: 'demo', powers: [], loading: false },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await settle()
|
||||
await wrapper.findAll('button').find((b) => b.text().includes('新增权限')).trigger('click')
|
||||
await settle()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length).toBe(1)
|
||||
await clickDelete(wrapper)
|
||||
await checkNoModal()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length, '单个删除后行应被移除').toBe(0)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('权限面板:批量删除选中多行', async () => {
|
||||
const wrapper = mount(ModulePowersPanel, {
|
||||
props: { moduleId: 'm1', moduleCode: 'demo', powers: [], loading: false },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await settle()
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await wrapper.findAll('button').find((b) => b.text().includes('新增权限')).trigger('click')
|
||||
await settle()
|
||||
}
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length).toBe(2)
|
||||
await selectRowCheckbox(wrapper, 0)
|
||||
await selectRowCheckbox(wrapper, 1)
|
||||
await clickToolbarBatch(wrapper)
|
||||
await checkNoModal()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length, '批量删除后应无行').toBe(0)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('多语言面板:单个删除不弹弹窗且行被移除', async () => {
|
||||
const wrapper = mount(ModuleI18nPanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
rows: [],
|
||||
languages: [
|
||||
{ code: 'zh-CN', name: '简体中文', default: true },
|
||||
{ code: 'en-US', name: 'English', default: false },
|
||||
],
|
||||
loading: false,
|
||||
},
|
||||
attachTo: document.body,
|
||||
})
|
||||
await settle()
|
||||
await wrapper.findAll('button').find((b) => b.text().includes('新增多语言')).trigger('click')
|
||||
await settle()
|
||||
const before = wrapper.findAll('.stk-tbody-main tr').length
|
||||
expect(before).toBeGreaterThan(0)
|
||||
await clickDelete(wrapper)
|
||||
await checkNoModal()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length, '单个删除后行应减少').toBe(before - 1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('多语言面板:批量删除选中多行', async () => {
|
||||
const wrapper = mount(ModuleI18nPanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
rows: [],
|
||||
languages: [
|
||||
{ code: 'zh-CN', name: '简体中文', default: true },
|
||||
{ code: 'en-US', name: 'English', default: false },
|
||||
],
|
||||
loading: false,
|
||||
},
|
||||
attachTo: document.body,
|
||||
})
|
||||
await settle()
|
||||
const before = wrapper.findAll('.stk-tbody-main tr').length
|
||||
for (let i = 0; i < 2; i++) {
|
||||
await wrapper.findAll('button').find((b) => b.text().includes('新增多语言')).trigger('click')
|
||||
await settle()
|
||||
}
|
||||
const total = wrapper.findAll('.stk-tbody-main tr').length
|
||||
expect(total).toBeGreaterThanOrEqual(before + 2)
|
||||
await selectRowCheckbox(wrapper, 0)
|
||||
await selectRowCheckbox(wrapper, 1)
|
||||
await clickToolbarBatch(wrapper)
|
||||
await checkNoModal()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr').length, '批量删除后应只剩受管行').toBe(before)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user