20260821230656

This commit is contained in:
oneao committed 2026-08-21 23:06:56 +08:00
1 parent 2e5d532f23
commit c43b4c8364
144 files changed
+7665 -242

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
import { nextIdApi } from '@/services/api'
export function cloneData(value) {
return JSON.parse(JSON.stringify(value))
}
export function diffRows(current, previous) {
const previousById = new Map(previous.map((item) => [String(item.b_id), item]))
const currentById = new Map(current.map((item) => [String(item.b_id), item]))
return {
inserts: current.filter((item) => !previousById.has(String(item.b_id))),
updates: current.filter((item) => {
const old = previousById.get(String(item.b_id))
return old && JSON.stringify(item) !== JSON.stringify(old)
}),
deletes: previous
.filter((item) => !currentById.has(String(item.b_id)))
.map((item) => ({ b_id: item.b_id })),
}
}
export async function allocateTemporaryIds(rowGroups) {
const temporaryIds = [
...new Set(
rowGroups
.flat()
.map((row) => String(row?.b_id ?? ''))
.filter((id) => /^-\d+$/.test(id)),
),
]
if (!temporaryIds.length) return new Map()
const res = await nextIdApi(temporaryIds.length)
const ids = Array.isArray(res.data) ? res.data : [res.data]
const mapping = new Map(temporaryIds.map((id, index) => [id, String(ids[index])]))
rowGroups.flat().forEach((row) => {
const id = mapping.get(String(row?.b_id))
if (id) row.b_id = id
})
return mapping
}
export function remapForeignKeys(rows, fields, mapping) {
rows.forEach((row) => {
fields.forEach((field) => {
const value = mapping.get(String(row[field]))
if (value) row[field] = value
})
})
}
export function tableChange(table, changes) {
return { table, key_field: 'b_id', ...changes }
}
export function hasChanges(request) {
return request.inserts?.length || request.updates?.length || request.deletes?.length
}
+120
View File
@@ -0,0 +1,120 @@
export function validateModuleCode(code) {
const value = String(code ?? '').trim()
if (!value) return '编码不能为空'
if (value.length < 2 || value.length > 50) return '编码长度需为 2-50 个字符'
if (!/^[a-z][a-z0-9_]*$/.test(value)) {
return '编码须以小写字母开头,且只能包含小写字母、数字和下划线'
}
return ''
}
export function getI18nNamespace(key) {
return String(key || '').split('.', 1)[0] || 'common'
}
export function isValidI18nKey(key) {
return /^[A-Za-z][A-Za-z0-9_.:-]{1,149}$/.test(String(key || ''))
}
export function buildModuleItemI18nKey(prefix, moduleCode, itemCode) {
const normalizedModuleCode = String(moduleCode || '').trim()
const normalizedItemCode = String(itemCode || '').trim()
return normalizedModuleCode && normalizedItemCode
? `${prefix}.${normalizedModuleCode}.${normalizedItemCode}`
: null
}
export function buildModuleI18nKey(moduleType, moduleCode) {
const normalizedModuleCode = String(moduleCode || '').trim()
if (!normalizedModuleCode) return null
const prefix = moduleType === 'data' || moduleType === 'query' ? 'module' : 'menu'
return `${prefix}.${normalizedModuleCode}`
}
export function syncModuleItemI18nKeys(rows, prefix, moduleCode, codeField) {
let changed = false
for (const row of rows || []) {
const key = buildModuleItemI18nKey(prefix, moduleCode, row?.[codeField])
if (row.b_i18n === key) continue
row.b_i18n = key
changed = true
}
return changed
}
export function groupI18nRows(rows) {
const groups = {}
rows.forEach((item) => {
const key = item.b_key
groups[key] ||= { key, values: {}, records: {} }
groups[key].values[item.b_locale] = item.b_value
groups[key].records[item.b_locale] = item
})
return groups
}
// 将同步字段生成的默认翻译条目并入透视行数组:
// - 已存在同 b_key 的行则补全其空语言值(不覆盖用户已填内容);
// - 否则新增一行;managedId 用于关联字段,使重命名字段时复用同一行(改名而非新增)。
// 返回新的数组,不修改入参。
export function mergeI18nDefaults(rows, entries) {
const next = [...rows]
const byKey = new Map(next.map((r) => [String(r.b_key), r]))
const managedIds = new Set(
next
.map((r) => r._managedId)
.filter(Boolean)
.map(String),
)
for (const e of entries || []) {
if (!e.key) continue
const existing = byKey.get(String(e.key))
if (existing) {
// 已存在同键行:仅补全空值,保留用户已配置的翻译
if (existing[e.locale] == null || existing[e.locale] === '') existing[e.locale] = e.value
continue
}
// 同键不存在,但该受管键已由其它行代表时不再新建,避免重复键
if (e.managedId && managedIds.has(String(e.managedId))) continue
const row = {
_id: 'i18n_' + Math.random().toString(36).slice(2, 10),
b_key: e.key,
_localeIds: {},
_managedId: e.managedId ? String(e.managedId) : undefined,
_syncInjected: e.managedId ? true : undefined,
[e.locale]: e.value,
}
next.push(row)
byKey.set(String(e.key), row)
if (e.managedId) managedIds.add(String(e.managedId))
}
return next
}
// 合并同 b_key 的重复透视行,保证一个键仅一行。
// 返回新的数组,不修改入参。
export function consolidatePivotRows(rows) {
const byKey = new Map()
const merged = []
for (const row of rows) {
const key = String(row.b_key || '')
const hit = byKey.get(key)
if (hit) {
for (const [k, v] of Object.entries(row)) {
if (k === '_id' || k === '_localeIds') continue
// 仅当目标为空(null/undefined/空串)且来源非空时才补全,保留首个非空值
if ((hit[k] == null || hit[k] === '') && v != null) hit[k] = v
}
Object.assign(hit._localeIds || (hit._localeIds = {}), row._localeIds || {})
if (row._managedId && !hit._managedId) hit._managedId = row._managedId
if (row._syncInjected && !hit._syncInjected) hit._syncInjected = row._syncInjected
if (row._defaultValue != null && hit._defaultValue == null)
hit._defaultValue = row._defaultValue
if (row._sortType && !hit._sortType) hit._sortType = row._sortType
} else {
byKey.set(key, row)
merged.push(row)
}
}
return merged
}
+12
View File
@@ -0,0 +1,12 @@
// 生成本地临时 ID,用于尚未保存到后端的子表新增行。
// 使用负数以区别于后端生成的雪花 ID(恒为正),保存时后端会替换为真实 ID。
let sequence = 0
/**
* 生成会话内唯一的临时 ID(负数字符串)。
* @returns {string} 形如 "-1"、"-2" 的临时 ID。
*/
export function createTempId() {
sequence += 1
return String(-sequence)
}