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>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,277 @@
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
}