Files
ERP/ofbiz-framework/plugins/modern-ui/app/src/components/erp/fieldOptions.ts
T
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

278 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { reactive } from 'vue'
import { getEntityOptions } from '../../services/api'
import type { OptionSource } from '../../types/api'
export const GENERATED_FIELD_WIDGETS = [
'display',
'drop-down',
'text',
'hidden',
'date-time',
'submit',
'lookup',
'hyperlink',
'ignored',
'unknown',
'textarea',
'check',
'file',
'radio',
'password'
] as const
export type SelectOption = {
label: string
value: unknown
source?: string
}
export type SelectOptionState = {
loading: boolean
reason: string
loaded: boolean
options: SelectOption[]
}
const controlOnlyWidgets = new Set(['hidden', 'ignored', 'submit'])
const readonlyWidgets = new Set(['display', 'display-entity', 'label', 'readonly', 'read-only'])
const hyperlinkWidgets = new Set(['hyperlink', 'link'])
const selectWidgets = new Set(['drop-down', 'select', 'combo-box', 'multi-select'])
const radioWidgets = new Set(['radio'])
const checkboxWidgets = new Set(['check', 'checkbox', 'boolean'])
const lookupWidgets = new Set(['lookup'])
const uploadWidgets = new Set(['file', 'upload', 'image', 'binary'])
const textareaWidgets = new Set(['textarea'])
const passwordWidgets = new Set(['password'])
function cleanFieldText(value: unknown) {
return String(value || '')
.replace(/\$\{uiLabelMap\.([^}]+)\}/g, '$1')
.replace(/\$\{([^}]+)\}/g, '$1')
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[-_]+/g, ' ')
.trim()
}
export function fieldName(field: Record<string, unknown>) {
return String(field.name || field.fieldName || field.id || field.parameterName || field.attributeName || '')
}
export function labelFor(field: Record<string, unknown>) {
return cleanFieldText(field.title || field.label || field.description || field.name || field.fieldName || field.id) || '字段'
}
export function fieldWidget(field: Record<string, unknown>) {
return String(field.widget || field.type || field.fieldType || 'text').trim().toLowerCase()
}
function fieldHaystack(field: Record<string, unknown>) {
return [
fieldWidget(field),
field.name,
field.fieldName,
field.id,
field.title,
field.label,
field.description,
field.tooltip,
field.target
].map((item) => String(item || '').toLowerCase()).join(' ')
}
export function isControlField(field: Record<string, unknown>) {
return controlOnlyWidgets.has(fieldWidget(field))
}
export function isReadonlyWidget(field: Record<string, unknown>) {
return readonlyWidgets.has(fieldWidget(field))
}
export function isHyperlinkWidget(field: Record<string, unknown>) {
const widget = fieldWidget(field)
if (hyperlinkWidgets.has(widget)) return true
return widget === 'unknown' && Boolean(field.target) && /link|url|uri|website/.test(fieldHaystack(field))
}
export function isSelectWidget(field: Record<string, unknown>) {
if (isRadioWidget(field) || isCheckboxWidget(field)) return false
return selectWidgets.has(fieldWidget(field))
|| Array.isArray(field.options) && field.options.length > 0
|| Array.isArray(field.optionSources) && field.optionSources.length > 0
}
export function isRadioWidget(field: Record<string, unknown>) {
return radioWidgets.has(fieldWidget(field))
}
export function isMultiSelectWidget(field: Record<string, unknown>) {
return fieldWidget(field) === 'multi-select'
}
export function isCheckboxWidget(field: Record<string, unknown>) {
return checkboxWidgets.has(fieldWidget(field))
}
export function isLookupWidget(field: Record<string, unknown>) {
return lookupWidgets.has(fieldWidget(field))
}
export function isUploadWidget(field: Record<string, unknown>) {
const haystack = fieldHaystack(field)
return uploadWidgets.has(fieldWidget(field))
|| /upload|file|binary|image|logo|picture|photo|avatar|vcard/.test(haystack)
}
export function isDateWidget(field: Record<string, unknown>) {
const haystack = fieldHaystack(field)
return fieldWidget(field).includes('date')
|| fieldWidget(field).includes('time')
|| /date|time|timestamp/.test(haystack)
}
export function datePickerTypeFor(field: Record<string, unknown>) {
const haystack = fieldHaystack(field)
return haystack.includes('time') || haystack.includes('timestamp') ? 'datetime' : 'date'
}
export function isTextareaWidget(field: Record<string, unknown>) {
return textareaWidgets.has(fieldWidget(field))
|| /description|comment|note|content|message|body|remark|memo/.test(fieldHaystack(field))
}
export function isPasswordWidget(field: Record<string, unknown>) {
return passwordWidgets.has(fieldWidget(field))
}
export function isNumericWidget(field: Record<string, unknown>) {
const haystack = fieldHaystack(field)
return ['number', 'integer', 'decimal', 'currency'].includes(fieldWidget(field))
|| /amount|balance|cost|price|total|quantity|count|limit|sequence|percent|rate|coeff|debit|credit|weight|height|width/.test(haystack)
|| /\bnum\b/.test(haystack)
}
export function inputTypeFor(field: Record<string, unknown>) {
const haystack = fieldHaystack(field)
if (/email|fromaddress|toaddress/.test(haystack)) return 'email'
if (/url|uri|website/.test(haystack)) return 'url'
if (/phone|telecom|tel/.test(haystack)) return 'tel'
return 'text'
}
export function shouldLoadOptions(field: Record<string, unknown>) {
return isSelectWidget(field) || isRadioWidget(field)
}
export function fieldValueText(value: unknown) {
if (value === undefined || value === null || value === '') return '-'
if (typeof value === 'boolean') return value ? '是' : '否'
if (Array.isArray(value)) return value.length ? `${value.length} 项` : '-'
if (typeof value === 'object') {
const entries = Object.entries(value as Record<string, unknown>)
.filter(([, entryValue]) => entryValue !== undefined && entryValue !== null && entryValue !== '' && typeof entryValue !== 'object')
.slice(0, 2)
if (!entries.length) return '已提供'
return entries.map(([key, entryValue]) => `${cleanFieldText(key)}: ${entryValue}`).join('')
}
return String(value)
}
export function staticOptionsFor(field: Record<string, unknown>): SelectOption[] {
const options = Array.isArray(field.options) ? field.options : []
return options
.map((option) => {
if (typeof option === 'string' || typeof option === 'number' || typeof option === 'boolean') {
return { label: cleanFieldText(option), value: option }
}
if (!option || typeof option !== 'object') return null
const item = option as Record<string, unknown>
const value = Object.prototype.hasOwnProperty.call(item, 'value')
? item.value
: item.key || item.id || item.name
const label = cleanFieldText(item.label || item.description || item.text || item.name || value)
return { label, value }
})
.filter((option): option is SelectOption => Boolean(option))
}
export function optionSourcesFor(field: Record<string, unknown>): OptionSource[] {
const sources: OptionSource[] = Array.isArray(field.optionSources) ? field.optionSources as OptionSource[] : []
if (field.optionSource && typeof field.optionSource === 'object') {
sources.push(field.optionSource as OptionSource)
}
const entityName = field.optionEntityName || field.entityName
if (entityName) {
sources.push({
type: 'entity-options',
entityName: String(entityName),
keyFieldName: String(field.keyFieldName || field.valueField || field.keyField || fieldName(field)),
description: String(field.descriptionFields || field.labelField || field.descriptionField || 'description')
})
}
return sources
}
export function firstEntityOptionSource(field: Record<string, unknown>) {
return optionSourcesFor(field).find((source) => source.type === 'entity-options' && source.entityName)
}
export function optionSourceReason(field: Record<string, unknown>, state?: SelectOptionState) {
if (state?.reason) return state.reason
const listSource = optionSourcesFor(field).find((source) => source.type === 'list-options')
if (listSource) {
return `动态列表 ${listSource.listName || fieldName(field)} 需要进入原业务上下文后加载`
}
const entitySource = firstEntityOptionSource(field)
if (entitySource) {
return '动态选项来自业务资料'
}
return ''
}
export function createOptionState() {
return reactive<Record<string, SelectOptionState>>({})
}
export async function ensureFieldOptions(
field: Record<string, unknown>,
states: Record<string, SelectOptionState>,
query = ''
) {
const name = fieldName(field)
const source = firstEntityOptionSource(field)
if (!name || !source) return
const state = states[name] || (states[name] = {
loading: false,
reason: '',
loaded: false,
options: []
})
if (state.loading) return
state.loading = true
state.reason = ''
try {
const result = await getEntityOptions(source, { query, pageSize: 60 })
state.options = Array.isArray(result.options)
? result.options.map((option) => ({ label: String(option.label || option.value || ''), value: option.value, source: 'entity-options' }))
: []
state.loaded = !result.unavailable
state.reason = result.unavailable ? String(result.reason || '动态选项暂不可用') : ''
} catch (error) {
state.loaded = false
state.reason = error instanceof Error ? error.message : '动态选项加载失败'
} finally {
state.loading = false
}
}
export function mergedOptionsFor(field: Record<string, unknown>, states: Record<string, SelectOptionState>) {
const seen = new Set<string>()
const merged: SelectOption[] = []
for (const option of [...staticOptionsFor(field), ...(states[fieldName(field)]?.options || [])]) {
const key = String(option.value)
if (seen.has(key)) continue
seen.add(key)
merged.push(option)
}
return merged
}