20260903221055

This commit is contained in:
oneao committed 2026-09-03 22:10:55 +08:00
1 parent 3d4e2c7413
commit 0928437c67
46 files changed
+2121 -1643

No files matched your search

@@ -13,11 +13,11 @@
* v-model:open="visible"
* data-code="s_i18n_type"
* :record="editingRow" // null=新增
* :before-save="handleBeforeSave" // 可选:返回 false 阻止保存;返回 { extraUpdates } 追加
* :on-before-save="handleBeforeSave" // 可选:返回 false 阻止保存;返回 { extraUpdates } 追加
* @saved="onSaved"
* />
*
* before-save(rowData, ctx): Promise<boolean | { extraUpdates?: [] }>
* on-before-save(rowData, ctx): Promise<boolean | { extraUpdates?: [] }>
* ctx = { isNew, record }
*/
import { computed, ref, watch } from 'vue'
@@ -34,7 +34,12 @@ import {
Select,
} from '@/components/ui'
import { loadDataApi, loadDataBySqlApi, nextIdApi, saveObjectApi } from '@/services/api'
import { identifierRe, isEmptyValue, parseOptions } from '@/components/fms-module-list/moduleListUtils'
import { diffRow } from '@/utils/dataChanges'
import {
identifierRe,
isEmptyValue,
parseOptions,
} from '@/components/fms-module-list/moduleListUtils'
const props = defineProps({
/** 受控打开 */
@@ -45,8 +50,8 @@ const props = defineProps({
record: { type: Object, default: null },
/** 弹窗标题;缺省按新增/编辑自动生成 */
title: { type: String, default: '' },
/** 保存前回调:返回 false 阻止;返回 { extraUpdates: [] } 追加到同表 updates */
beforeSave: { type: Function, default: null },
/** 保存前业务钩子:返回 false 阻止;返回 { extraUpdates: [] } 追加到同表 updates */
onBeforeSave: { type: Function, default: null },
/** 额外禁用规则:(fieldName, ctx) => boolean,ctx={ isNew, record, formData } */
fieldDisabled: { type: Function, default: null },
/** 弹窗宽度(px 或 CSS 宽度);缺省(0)按表单栅格布局自动推断 */
@@ -67,6 +72,8 @@ const groups = ref([]) // s_field_group
const editRows = ref([]) // s_field_edit 可见行(含关联字段引用)
const formData = ref({})
const keyField = ref('b_id')
/** 编辑基线(origin):打开弹窗时对 record 深拷贝快照,保存只提交与它的差异;新建模式为 null */
const originRef = ref(null)
/* ================= 分组结构推导(与 FormPreview 一致) ================= */
const sections = computed(() => {
@@ -149,6 +156,10 @@ async function loadConfig() {
/** 初始化表单数据:编辑用 record 回填,新增按默认值 */
function buildFormData() {
const isNew = !props.record
// 编辑态保留一份不可直接编辑的 origin 基线(深拷贝),保存时与其 diff;
// 新建模式没有真实基线,保存走整行 insert
originRef.value = isNew ? null : JSON.parse(JSON.stringify(props.record || {}))
const source = props.record || {}
const data = {}
for (const row of editRows.value) {
@@ -244,11 +255,21 @@ const resolvedWidth = computed(() => {
return (WIDTH_TIERS.find((t) => n <= t.maxFields) || WIDTH_TIERS[WIDTH_TIERS.length - 1]).width
})
/* ================= 保存 ================= */
/* ================= 保存(handleSave → beforeSave → buildSaveData → 接口 → afterSave) ================= */
/** 保存过程上下文:beforeSave 阶段准备的数据,buildSaveData / afterSave 阶段消费 */
let saveCtx = { isNew: false, row: null, extraUpdates: [] }
/** 模块保存表(s_module.b_savetable 优先,其次 b_viewtable) */
function saveTableName() {
return String(moduleMeta.value?.b_savetable || moduleMeta.value?.b_viewtable || '')
}
/** 构造完整合并行(含主键;checkbox→0/1、''→null 归一化),供保存前准备与 diff 使用 */
function collectRowData() {
const isNew = !props.record
const row = {}
if (isNew) row[keyField.value] = null // 主键待 nextIdApi 分配
if (isNew)
row[keyField.value] = null // 主键待 nextIdApi 分配
else row[keyField.value] = props.record[keyField.value]
for (const editRow of editRows.value) {
@@ -262,8 +283,8 @@ function collectRowData() {
return { row, isNew }
}
async function handleSave() {
if (saving.value) return
/** 保存前校验、数据准备与业务钩子;返回 false 终止保存 */
async function beforeSave() {
// 必填校验:s_field_edit.b_required=1 且值为空 → 提示并定位
for (const editRow of editRows.value) {
if (Number(editRow.b_required) !== 1) continue
@@ -271,60 +292,101 @@ async function handleSave() {
const value = formData.value[editRow.field.b_field]
if (isEmptyValue(value)) {
Message.warning(`请填写「${fieldLabel(editRow)}」`)
return
return false
}
}
if (!saveTableName()) {
Message.error('该模块未配置保存表')
return false
}
const { row, isNew } = collectRowData()
// 新增时排序字段未填 → 自动取当前最大值 + 10
if (
isNew &&
props.orderField &&
identifierRe.test(props.orderField) &&
!Number(row[props.orderField])
) {
try {
const viewTable = String(moduleMeta.value?.b_viewtable || '')
if (viewTable) {
const res = await loadDataBySqlApi(
`SELECT ISNULL(MAX([${props.orderField}]), 0) AS mx FROM [${viewTable}]`,
)
row[props.orderField] = (Number(res.data?.[0]?.mx) || 0) + 10
}
} catch {
// 查最大失败时不阻塞,让用户手填或存 0
}
}
// 新增时分配主键
if (isNew && !row[keyField.value]) {
row[keyField.value] = String((await nextIdApi()).data)
}
// 外部业务钩子:接收完整合并行,返回 false 终止;返回 { extraUpdates } 追加到同表请求
let extraUpdates = []
if (props.onBeforeSave) {
const result = await props.onBeforeSave(row, { isNew, record: props.record })
if (result === false) {
saveCtx.row = null
return false
}
if (result?.extraUpdates?.length) extraUpdates = result.extraUpdates
}
saveCtx = { isNew, row, extraUpdates }
return true
}
/** 组装保存请求:编辑态用 diffRow 与 origin 做字段级对比,只提交变化字段;无变化返回 null(不调保存接口) */
function buildSaveData() {
const { isNew, row, extraUpdates } = saveCtx
if (!row) return null
const request = { table: saveTableName(), key_field: keyField.value }
if (isNew) {
request.inserts = [row]
if (extraUpdates.length) request.updates = extraUpdates
return [request]
}
// 编辑态:字段级 diff(checkbox/null/数字归一已内置于 diffRow)
const changes = diffRow({ current: row, original: originRef.value, primaryKey: keyField.value })
if (!changes.hasChanges && !extraUpdates.length) return null
// 本行有变化:主键 + 变化字段;仅外部钩子追加了 extraUpdates:只提交追加行,避免仅主键的空更新
request.updates = changes.hasChanges ? [...changes.updates, ...extraUpdates] : extraUpdates
return [request]
}
/** 保存成功后的提示、事件与关闭 */
function afterSave() {
const { isNew, row } = saveCtx
Message.success(isNew ? '新增成功' : '保存成功')
emit('saved', { row, isNew })
emit('update:open', false)
}
async function handleSave() {
if (saving.value) return false
saveCtx = { isNew: false, row: null, extraUpdates: [] }
saving.value = true
try {
const { row, isNew } = collectRowData()
const table = String(moduleMeta.value.b_savetable || moduleMeta.value.b_viewtable || '')
if (!table) {
Message.error('该模块未配置保存表')
return
if (!(await beforeSave())) return false
const saveData = buildSaveData()
if (!saveData) {
// 编辑态无任何字段变化:按规范不调用保存接口,直接提示并关闭
Message.info('没有需要保存的变更')
emit('update:open', false)
return true
}
// 新增时排序字段未填 → 自动取当前最大值 + 10
if (isNew && props.orderField && identifierRe.test(props.orderField) && !Number(row[props.orderField])) {
try {
const viewTable = String(moduleMeta.value.b_viewtable || '')
if (viewTable) {
const res = await loadDataBySqlApi(
`SELECT ISNULL(MAX([${props.orderField}]), 0) AS mx FROM [${viewTable}]`,
)
row[props.orderField] = (Number(res.data?.[0]?.mx) || 0) + 10
}
} catch {
// 查最大失败时不阻塞,让用户手填或存 0
}
}
// 新增时分配主键
if (isNew && !row[keyField.value]) {
row[keyField.value] = String((await nextIdApi()).data)
}
// 业务前处理钩子
let extraUpdates = []
if (props.beforeSave) {
const result = await props.beforeSave(row, { isNew, record: props.record })
if (result === false) return
if (result?.extraUpdates?.length) extraUpdates = result.extraUpdates
}
const request = { table, key_field: keyField.value }
if (isNew) {
request.inserts = [row]
if (extraUpdates.length) request.updates = extraUpdates
} else {
request.updates = [row, ...extraUpdates]
}
await saveObjectApi([request])
Message.success(isNew ? '新增成功' : '保存成功')
emit('saved', { row, isNew })
emit('update:open', false)
await saveObjectApi(saveData)
await afterSave()
return true
} catch (e) {
Message.error('保存失败:' + (e?.message || e))
return false
} finally {
saving.value = false
}
@@ -383,12 +445,7 @@ const selectOptionsOf = (field) => parseOptions(field).options
:model-value="Number(formData[row.field.b_field]) === 1"
disabled
/>
<Input
v-else
:model-value="formData[row.field.b_field] ?? ''"
disabled
readonly
/>
<Input v-else :model-value="formData[row.field.b_field] ?? ''" disabled readonly />
</template>
<Input
v-else-if="row.field.b_type === 'input'"
@@ -426,7 +483,9 @@ const selectOptionsOf = (field) => parseOptions(field).options
<Input
v-else
v-model="formData[row.field.b_field]"
:type="row.field.b_type === 'number' || row.field.b_type === 'money' ? 'number' : 'default'"
:type="
row.field.b_type === 'number' || row.field.b_type === 'money' ? 'number' : 'default'
"
:placeholder="row.b_placeholder"
/>
</FormItem>