20260903221055
This commit is contained in:
1 parent
3d4e2c7413
commit
0928437c67
46 files changed
+2121
-1643
No files matched your search
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* 页签缓存标识:把路由 fullPath 映射为 keep-alive include 可匹配的唯一组件名。
|
||||
*
|
||||
* 背景:keep-alive 的 include/exclude 只能按「组件名」匹配,无法按 :key(fullPath)精确销毁。
|
||||
* 对于「同一组件被不同参数复用」(如 /detail/1、/detail/2 共用 DetailPage)的场景,
|
||||
* 需要让每个 fullPath 拥有独立的组件类型与组件名,include 才能按参数精确销毁其实例,
|
||||
* 同时不同参数天然成为多个独立缓存。
|
||||
*/
|
||||
export function cacheKeyOf(fullPath) {
|
||||
return `page_${encodeURIComponent(fullPath)}`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 本地临时 ID 使用负数,避免与后端生成的正数 ID 冲突。
|
||||
let tempIdSequence = 0
|
||||
|
||||
export function createTempId() {
|
||||
tempIdSequence += 1
|
||||
return String(-tempIdSequence)
|
||||
}
|
||||
@@ -55,3 +55,64 @@ export function tableChange(table, changes) {
|
||||
export function hasChanges(request) {
|
||||
return request.inserts?.length || request.updates?.length || request.deletes?.length
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条记录字段级差异比对(主表 / 编辑弹窗;行集合请用 diffRows)。
|
||||
* @param {{ current: object, original?: object|null, primaryKey?: string, ignoreFields?: string[] }} input
|
||||
* @returns {{ hasChanges: boolean, inserts: [], updates: [], deletes: [] }}
|
||||
* updates 只携带「变化的字段 + 主键」(后端按提供列 SET,字段级更安全);
|
||||
* 无基线或无主键 → 视为新建(inserts 为过滤后的整行)。
|
||||
*/
|
||||
export function diffRow({ current, original, primaryKey = 'b_id', ignoreFields = [] }) {
|
||||
const ignoreSet = new Set(ignoreFields)
|
||||
const pk = primaryKey
|
||||
// 与旧系统一致:版本字段不参与内容比较(提交时也不携带,交由后端维护)
|
||||
const VERSION_FIELDS = new Set(['b_version', 'mx_version'])
|
||||
|
||||
const isNumberValue = (value) => {
|
||||
if (typeof value === 'number') return !Number.isNaN(value)
|
||||
if (typeof value === 'string') {
|
||||
if (value.trim() === '') return false
|
||||
return !Number.isNaN(Number(value))
|
||||
}
|
||||
return false
|
||||
}
|
||||
// null / undefined / 空串 等价;两边都是数字时按数值比较(40 === '40.00')
|
||||
const isEqualValue = (a, b) => {
|
||||
const norm = (v) => (v === undefined || v === null || v === '' ? '' : v)
|
||||
const na = norm(a)
|
||||
const nb = norm(b)
|
||||
if (isNumberValue(na) && isNumberValue(nb)) return Math.abs(Number(na) - Number(nb)) < 0.000001
|
||||
return JSON.stringify(na) === JSON.stringify(nb)
|
||||
}
|
||||
// 行内下划线前缀(_*)与视图前缀(v_*)字段不参与比较与提交
|
||||
const isIgnorableKey = (key) =>
|
||||
key.startsWith('_') || key.startsWith('v_') || ignoreSet.has(key) || VERSION_FIELDS.has(key)
|
||||
|
||||
const result = { hasChanges: false, inserts: [], updates: [], deletes: [] }
|
||||
const cur = current || {}
|
||||
|
||||
// 无基线或基线无主键 → 新建整行(剔除 _/v_/ignoreFields 前缀字段)
|
||||
if (!original || original[pk] === undefined || original[pk] === null || original[pk] === '') {
|
||||
const insert = {}
|
||||
for (const [key, value] of Object.entries(cur)) {
|
||||
if (key.startsWith('_') || key.startsWith('v_') || ignoreSet.has(key)) continue
|
||||
insert[key] = value
|
||||
}
|
||||
result.hasChanges = true
|
||||
result.inserts.push(insert)
|
||||
return result
|
||||
}
|
||||
|
||||
// 修改:只携带变化字段 + 主键
|
||||
const changed = {}
|
||||
for (const key of Object.keys(cur)) {
|
||||
if (key === pk || isIgnorableKey(key)) continue
|
||||
if (!isEqualValue(cur[key], original[key])) changed[key] = cur[key]
|
||||
}
|
||||
if (Object.keys(changed).length) {
|
||||
result.hasChanges = true
|
||||
result.updates.push({ [pk]: cur[pk], ...changed })
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// 生成本地临时 ID,用于尚未保存到后端的子表新增行。
|
||||
// 使用负数以区别于后端生成的雪花 ID(恒为正),保存时后端会替换为真实 ID。
|
||||
let sequence = 0
|
||||
|
||||
/**
|
||||
* 生成会话内唯一的临时 ID(负数字符串)。
|
||||
* @returns {string} 形如 "-1"、"-2" 的临时 ID。
|
||||
*/
|
||||
export function createTempId() {
|
||||
sequence += 1
|
||||
return String(-sequence)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { computed, readonly, ref } from 'vue'
|
||||
|
||||
// 翻译目录独立于 Pinia:组件、服务和非组件代码都可以复用,切换目录时保持响应式。
|
||||
const localeRef = ref('zh-CN')
|
||||
const messagesRef = ref({})
|
||||
|
||||
export function translate(key, fallback = '') {
|
||||
const normalizedKey = String(key || '')
|
||||
const value = messagesRef.value[normalizedKey]
|
||||
if (value !== undefined && value !== null && String(value) !== '') return String(value)
|
||||
return fallback || normalizedKey
|
||||
}
|
||||
|
||||
export function setLocale(locale) {
|
||||
localeRef.value = String(locale || '')
|
||||
}
|
||||
|
||||
export function setMessages(messages, locale = undefined) {
|
||||
messagesRef.value = messages && typeof messages === 'object' ? { ...messages } : {}
|
||||
if (locale !== undefined) setLocale(locale)
|
||||
}
|
||||
|
||||
export function clearMessages() {
|
||||
messagesRef.value = {}
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
return {
|
||||
locale: readonly(localeRef),
|
||||
messages: readonly(messagesRef),
|
||||
t: translate,
|
||||
setLocale,
|
||||
setMessages,
|
||||
clearMessages,
|
||||
localeValue: computed(() => localeRef.value),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user