20260903221055
This commit is contained in:
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>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRoute } from 'vue-router'
|
||||
import { Columns3, RefreshCw, Save } from '@lucide/vue'
|
||||
import { Button, Message, Pagination, Spin } from '@/components/ui'
|
||||
import { loadDataApi, nextIdApi, pageDataApi, saveObjectApi } from '@/services/api'
|
||||
import { useI18n } from '@/utils/translate'
|
||||
import { useI18nStore } from '@/stores/i18n'
|
||||
import {
|
||||
identifierRe,
|
||||
buildQueryCondition,
|
||||
@@ -30,8 +30,8 @@ const props = defineProps({
|
||||
editable: { type: Boolean, default: false },
|
||||
// 行拖拽排序:开启后显示拖拽手柄,拖拽按 orderField 重排并纳入脏保存。
|
||||
rowDraggable: { type: Boolean, default: false },
|
||||
// 行拖拽排序字段,仅 rowDraggable 时生效。
|
||||
orderField: { type: String, default: 'b_xh' },
|
||||
// 行拖拽排序字段;未显式传入时不启用业务序号排序。
|
||||
orderField: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -44,7 +44,7 @@ const emit = defineEmits([
|
||||
])
|
||||
const slots = useSlots()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18nStore()
|
||||
const tableRef = ref(null)
|
||||
// keep-alive 中的缓存实例必须保留各自的路由上下文,不能跟随全局 route 变化。
|
||||
const instanceRoutePath = route.path
|
||||
@@ -78,6 +78,11 @@ const pagination = reactive({
|
||||
let dataRequestVersion = 0
|
||||
|
||||
const loading = computed(() => configLoading.value || dataLoading.value)
|
||||
const effectiveOrderField = computed(() => {
|
||||
const field = String(props.orderField || '').trim()
|
||||
return ['b_xh', 'mx_xh'].includes(field) ? field : ''
|
||||
})
|
||||
const effectiveRowDraggable = computed(() => props.rowDraggable && Boolean(effectiveOrderField.value))
|
||||
|
||||
// ── 外部工具 ──
|
||||
const moreMenuItems = computed(() => [
|
||||
@@ -513,15 +518,16 @@ async function addRow() {
|
||||
const newId = String(idRes.data)
|
||||
const newRow = { [keyField.value]: newId }
|
||||
// 排序字段取当前最大值 + 10;checkbox 字段默认 0。
|
||||
if (props.orderField && identifierRe.test(props.orderField)) {
|
||||
const orderField = effectiveOrderField.value
|
||||
if (orderField) {
|
||||
let maxXh = 0
|
||||
for (const row of dataSource.value) {
|
||||
maxXh = Math.max(maxXh, Number(row[props.orderField]) || 0)
|
||||
maxXh = Math.max(maxXh, Number(row[orderField]) || 0)
|
||||
}
|
||||
newRow[props.orderField] = maxXh + 10
|
||||
newRow[orderField] = maxXh + 10
|
||||
}
|
||||
for (const field of activeFields.value) {
|
||||
if (field.b_field === keyField.value || field.b_field === props.orderField) continue
|
||||
if (field.b_field === keyField.value || field.b_field === orderField) continue
|
||||
if (newRow[field.b_field] !== undefined) continue
|
||||
if (field.b_type === 'checkbox') newRow[field.b_field] = 0
|
||||
}
|
||||
@@ -629,8 +635,8 @@ async function deleteSelected(rowKeys) {
|
||||
// editable 模式:标记为脏、随 saveChanges 一起提交;
|
||||
// 非 editable 模式:直接持久化(行内排序是独立操作,无统一保存入口)。
|
||||
function handleRowOrderChange(orderedRows) {
|
||||
if (!props.rowDraggable || !props.orderField) return
|
||||
if (!identifierRe.test(props.orderField)) return
|
||||
const orderField = effectiveOrderField.value
|
||||
if (!effectiveRowDraggable.value) return
|
||||
const rowByOld = new Map(dataSource.value.map((row) => [String(resolveRowKey(row)), row]))
|
||||
const changedKeys = new Set()
|
||||
const nextRows = []
|
||||
@@ -638,8 +644,8 @@ function handleRowOrderChange(orderedRows) {
|
||||
for (const row of orderedRows) {
|
||||
const key = String(resolveRowKey(row))
|
||||
const oldRow = rowByOld.get(key)
|
||||
const oldValue = oldRow ? Number(oldRow[props.orderField]) || 0 : 0
|
||||
const copy = { ...row, [props.orderField]: order }
|
||||
const oldValue = oldRow ? Number(oldRow[orderField]) || 0 : 0
|
||||
const copy = { ...row, [orderField]: order }
|
||||
nextRows.push(copy)
|
||||
if (oldValue !== order) changedKeys.add(key)
|
||||
order += 10
|
||||
@@ -650,7 +656,7 @@ function handleRowOrderChange(orderedRows) {
|
||||
const dirtyFieldsByKey = new Map()
|
||||
for (const key of changedKeys) {
|
||||
const fields = dirtyFieldsByKey.get(key) || new Set()
|
||||
fields.add(props.orderField)
|
||||
fields.add(orderField)
|
||||
dirtyFieldsByKey.set(key, fields)
|
||||
}
|
||||
for (const [key, fields] of dirtyFieldsByKey) {
|
||||
@@ -671,7 +677,8 @@ function handleRowOrderChange(orderedRows) {
|
||||
|
||||
/** 非 editable 模式的行拖拽排序持久化:只提交 orderField 变化的行 */
|
||||
async function persistRowOrder(changedKeys) {
|
||||
if (!changedKeys.size || saving.value) return
|
||||
const orderField = effectiveOrderField.value
|
||||
if (!changedKeys.size || saving.value || !orderField) return
|
||||
const table = resolveSaveTable()
|
||||
if (!table) {
|
||||
Message.error('该模块未配置保存表,无法保存排序')
|
||||
@@ -688,7 +695,7 @@ async function persistRowOrder(changedKeys) {
|
||||
for (const key of changedKeys) {
|
||||
const row = rowByKey.get(key)
|
||||
if (!row) continue
|
||||
updates.push({ [keyField.value]: row[keyField.value], [props.orderField]: row[props.orderField] })
|
||||
updates.push({ [keyField.value]: row[keyField.value], [orderField]: row[orderField] })
|
||||
}
|
||||
if (!updates.length) return
|
||||
await saveObjectApi([{ table, key_field: keyField.value, updates }])
|
||||
@@ -815,7 +822,7 @@ defineExpose({
|
||||
:expandable="expandable"
|
||||
:inline-action-limit="inlineActionLimit"
|
||||
:editable="editable"
|
||||
:row-draggable="rowDraggable"
|
||||
:row-draggable="effectiveRowDraggable"
|
||||
@row-click="$emit('row-click', $event)"
|
||||
@cell-click="$emit('cell-click', $event)"
|
||||
@row-action="$emit('row-action', $event)"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, h, ref, shallowRef, useSlots, watch } from 'vue'
|
||||
import { Checkbox } from '@/components/ui'
|
||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||
import { useI18n } from '@/utils/translate'
|
||||
import { useI18nStore } from '@/stores/i18n'
|
||||
import { formatDateValue, parseOptions } from './moduleListUtils'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -31,7 +31,9 @@ const emit = defineEmits([
|
||||
'cell-change',
|
||||
'row-order-change',
|
||||
])
|
||||
const { t, localeValue } = useI18n()
|
||||
const i18nStore = useI18nStore()
|
||||
const { t } = i18nStore
|
||||
const localeValue = computed(() => i18nStore.locale)
|
||||
const slots = useSlots()
|
||||
const tableRef = ref(null)
|
||||
const columns = shallowRef([])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ChevronDown, ChevronUp, Ellipsis, Search } from '@lucide/vue'
|
||||
import { Button, Dropdown, FormItem, Select } from '@/components/ui'
|
||||
import { useI18n } from '@/utils/translate'
|
||||
import { useI18nStore } from '@/stores/i18n'
|
||||
import FmsQueryControl from './FmsQueryControl.vue'
|
||||
import { defaultQueryValue, emptyQueryValue } from './moduleListUtils'
|
||||
|
||||
@@ -13,7 +13,7 @@ const props = defineProps({
|
||||
defaultAdvancedCount: { type: Number, default: 6 },
|
||||
})
|
||||
const emit = defineEmits(['search', 'reset', 'more-action'])
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18nStore()
|
||||
const filters = reactive({})
|
||||
const quickFieldName = ref('')
|
||||
const quickValue = ref(undefined)
|
||||
|
||||
@@ -4,29 +4,6 @@ export const identifierRe = /^[A-Za-z_][A-Za-z0-9_]*$/
|
||||
|
||||
const padDatePart = (number) => String(number).padStart(2, '0')
|
||||
|
||||
// 序号字段按保存表主键的命名域推断:b_id(系统/基础表)→ b_xh,subid(业务子表)→ mx_xh。
|
||||
const seqFieldByKeyField = new Map([
|
||||
['b_id', 'b_xh'],
|
||||
['subid', 'mx_xh'],
|
||||
])
|
||||
|
||||
/**
|
||||
* 解析模块的序号字段(用于行拖拽重排、新增自动取 MAX+10)。
|
||||
* 显式传入 orderField 时直接使用(页面覆盖口);
|
||||
* 否则按主键域推断,且推断结果必须已登记为模块的数值字段,
|
||||
* 否则返回 ''(不启用,避免给无序号列的表塞字段,如日志表)。
|
||||
*/
|
||||
export function resolveSeqField(orderField, module, fields) {
|
||||
const explicit = String(orderField || '').trim()
|
||||
if (identifierRe.test(explicit)) return explicit
|
||||
const inferred = seqFieldByKeyField.get(String(module?.b_keyfield || '').trim())
|
||||
if (!inferred) return ''
|
||||
const field = (fields || []).find((item) => item.b_field === inferred)
|
||||
if (!field) return ''
|
||||
if (field.b_type !== 'number' && field.b_type !== 'money') return ''
|
||||
return inferred
|
||||
}
|
||||
|
||||
export function escapeSqlText(value) {
|
||||
return String(value).replaceAll("'", "''")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user