| <template> | ||
| <view class="flm-alert" :class="alertClasses" :style="alertStyle"> | ||
| <view v-if="showIcon" class="flm-alert-icon">{{ iconChar }}</view> | ||
| <view class="flm-alert-body"> | ||
| <text v-if="titleText" class="flm-alert-title">{{ titleText }}</text> | ||
| <text v-if="descriptionText" class="flm-alert-desc">{{ descriptionText }}</text> | ||
| </view> | ||
| <text v-if="closableEnabled && !dismissed" class="flm-alert-close" @tap="dismiss">×</text> | ||
| </view> | ||
| </template> | ||
| <script lang="ts" setup> | ||
| import { computed, ref } from 'vue' | ||
| type AlertType = 'success' | 'warning' | 'info' | 'error' | ||
| type AlertEffect = 'light' | 'dark' | ||
| type AlertSize = 'large' | 'default' | 'small' | 'mini' | ||
| const props = withDefaults( | ||
| defineProps<{ | ||
| config?: Record<string, unknown> | ||
| }>(), | ||
| { | ||
| config: () => ({}) | ||
| } | ||
| ) | ||
| const dismissed = ref(false) | ||
| const cfg = computed(() => props.config || {}) | ||
| const alertType = computed((): AlertType => { | ||
| const t = cfg.value.type | ||
| if (t === 'success' || t === 'warning' || t === 'info' || t === 'error') return t | ||
| return 'info' | ||
| }) | ||
| const alertEffect = computed((): AlertEffect => { | ||
| const e = cfg.value.effect | ||
| return e === 'dark' ? 'dark' : 'light' | ||
| }) | ||
| const alertSize = computed((): AlertSize => { | ||
| const s = cfg.value.size | ||
| if (s === 'large' || s === 'default' || s === 'small' || s === 'mini') return s | ||
| return 'mini' | ||
| }) | ||
| const titleText = computed(() => { | ||
| const t = cfg.value.title | ||
| return t != null && String(t).trim() !== '' ? String(t) : '' | ||
| }) | ||
| const descriptionText = computed(() => { | ||
| const d = cfg.value.description | ||
| return d != null && String(d).trim() !== '' ? String(d) : '' | ||
| }) | ||
| const showIcon = computed(() => cfg.value['show-icon'] !== false && cfg.value.showIcon !== false) | ||
| const closableEnabled = computed(() => cfg.value.closable !== false) | ||
| const centerEnabled = computed(() => cfg.value.center === true) | ||
| const alertClasses = computed(() => [ | ||
| `flm-alert--${alertType.value}`, | ||
| `flm-alert--${alertEffect.value}`, | ||
| `flm-alert--${alertSize.value}`, | ||
| centerEnabled.value ? 'flm-alert--center' : '', | ||
| cfg.value.class != null ? String(cfg.value.class) : '' | ||
| ]) | ||
| const alertStyle = computed(() => { | ||
| const style = cfg.value.style | ||
| if (style && typeof style === 'object' && !Array.isArray(style)) { | ||
| return style as Record<string, string | number> | ||
| } | ||
| if (typeof style === 'string') return style | ||
| return undefined | ||
| }) | ||
| const iconChar = computed(() => { | ||
| const map: Record<AlertType, string> = { | ||
| success: '✓', | ||
| warning: '!', | ||
| info: 'i', | ||
| error: '×' | ||
| } | ||
| return map[alertType.value] | ||
| }) | ||
| function dismiss() { | ||
| dismissed.value = true | ||
| } | ||
| </script> | ||
| <style scoped> | ||
| .flm-alert { | ||
| display: flex; | ||
| flex-direction: row; | ||
| align-items: flex-start; | ||
| gap: 12rpx; | ||
| padding: 16rpx 20rpx; | ||
| border-radius: 8rpx; | ||
| border-width: 1px; | ||
| border-style: solid; | ||
| box-sizing: border-box; | ||
| width: 100%; | ||
| } | ||
| .flm-alert--light.flm-alert--success { | ||
| background-color: #f0f9eb; | ||
| border-color: #e1f3d8; | ||
| color: #67c23a; | ||
| } | ||
| .flm-alert--light.flm-alert--warning { | ||
| background-color: #fdf6ec; | ||
| border-color: #faecd8; | ||
| color: #e6a23c; | ||
| } | ||
| .flm-alert--light.flm-alert--info { | ||
| background-color: #f4f4f5; | ||
| border-color: #e9e9eb; | ||
| color: #909399; | ||
| } | ||
| .flm-alert--light.flm-alert--error { | ||
| background-color: #fef0f0; | ||
| border-color: #fde2e2; | ||
| color: #f56c6c; | ||
| } | ||
| .flm-alert--dark.flm-alert--success { | ||
| background-color: #67c23a; | ||
| border-color: #67c23a; | ||
| color: #ffffff; | ||
| } | ||
| .flm-alert--dark.flm-alert--warning { | ||
| background-color: #e6a23c; | ||
| border-color: #e6a23c; | ||
| color: #ffffff; | ||
| } | ||
| .flm-alert--dark.flm-alert--info { | ||
| background-color: #909399; | ||
| border-color: #909399; | ||
| color: #ffffff; | ||
| } | ||
| .flm-alert--dark.flm-alert--error { | ||
| background-color: #f56c6c; | ||
| border-color: #f56c6c; | ||
| color: #ffffff; | ||
| } | ||
| .flm-alert--mini { | ||
| padding: 8rpx 16rpx; | ||
| } | ||
| .flm-alert--small .flm-alert-title, | ||
| .flm-alert--mini .flm-alert-title { | ||
| font-size: 26rpx; | ||
| } | ||
| .flm-alert--large .flm-alert-title { | ||
| font-size: 32rpx; | ||
| } | ||
| .flm-alert--center { | ||
| align-items: center; | ||
| } | ||
| .flm-alert-icon { | ||
| flex-shrink: 0; | ||
| font-size: 28rpx; | ||
| line-height: 1.2; | ||
| font-weight: 600; | ||
| } | ||
| .flm-alert-body { | ||
| flex: 1; | ||
| min-width: 0; | ||
| } | ||
| .flm-alert-title { | ||
| display: block; | ||
| font-size: 28rpx; | ||
| font-weight: 600; | ||
| line-height: 1.4; | ||
| } | ||
| .flm-alert-desc { | ||
| display: block; | ||
| font-size: 26rpx; | ||
| line-height: 1.5; | ||
| margin-top: 6rpx; | ||
| white-space: pre-line; | ||
| } | ||
| .flm-alert-close { | ||
| flex-shrink: 0; | ||
| font-size: 36rpx; | ||
| line-height: 1; | ||
| padding: 0 4rpx; | ||
| opacity: 0.7; | ||
| } | ||
| </style> |
+1
-1
| { | ||
| "name": "flame-uni", | ||
| "version": "0.0.2", | ||
| "version": "0.0.3", | ||
| "description": "基于 uni-app 的移动端动态表单组件库(与 flame-plus flmDynamicForm 配置协议对齐)", | ||
@@ -5,0 +5,0 @@ "keywords": ["uni-app", "uni-ui", "flame", "dynamic-form"], |
| <template> | ||
| <view class="flm-cascader-root"> | ||
| <view v-if="multiple" class="flmCascader-tip">多选级联请使用后续版本</view> | ||
| <view v-if="multiple" class="flmCascader-multiple"> | ||
| <text v-if="multipleSummary" class="flmCascader-multiple-summary">{{ multipleSummary }}</text> | ||
| <uni-data-checkbox | ||
| multiple | ||
| mode="list" | ||
| :localdata="leafLocaldata" | ||
| :model-value="multipleBindKeys" | ||
| :disabled="pickerReadonly" | ||
| @update:model-value="onMultipleUpdate" | ||
| /> | ||
| </view> | ||
| <uni-data-picker | ||
@@ -98,7 +108,2 @@ v-else-if="useUniPicker" | ||
| const useUniPicker = computed(() => !multiple.value && !emitPath.value) | ||
| const placeholderText = computed(() => String(cfg.value.placeholder || '请选择')) | ||
| const popupTitle = computed(() => String(cfg.value.popupTitle || cfg.value['popup-title'] || '请选择')) | ||
| const splitText = computed(() => { | ||
@@ -115,2 +120,77 @@ const sep = cfg.value.separator | ||
| function nodeChildren(node: Record<string, unknown> | undefined) { | ||
| const childRaw = node?.[childrenKey.value] | ||
| return Array.isArray(childRaw) && childRaw.length ? (childRaw as Record<string, unknown>[]) : null | ||
| } | ||
| type LeafRow = { text: string; value: string; rawValue: unknown; disable: boolean } | ||
| function flattenLeafRows( | ||
| nodes: Record<string, unknown>[], | ||
| prefixLabels: string[] = [] | ||
| ): LeafRow[] { | ||
| const rows: LeafRow[] = [] | ||
| for (const node of nodes) { | ||
| const label = String(node[labelKey.value] ?? '') | ||
| const pathLabels = [...prefixLabels, label].filter(Boolean) | ||
| const children = nodeChildren(node) | ||
| if (!children) { | ||
| const leafValue = node[valueKey.value] | ||
| rows.push({ | ||
| text: pathLabels.join(splitText.value), | ||
| value: String(leafValue), | ||
| rawValue: leafValue, | ||
| disable: Boolean(node.disabled ?? node.disable) | ||
| }) | ||
| continue | ||
| } | ||
| rows.push(...flattenLeafRows(children, pathLabels)) | ||
| } | ||
| return rows | ||
| } | ||
| const leafRows = computed(() => flattenLeafRows(options.value)) | ||
| const leafLocaldata = computed(() => | ||
| leafRows.value.map((row) => ({ | ||
| text: row.text, | ||
| value: row.value, | ||
| disable: row.disable | ||
| })) | ||
| ) | ||
| const multipleModelValues = computed(() => { | ||
| const current = effectiveModelValue.value | ||
| return Array.isArray(current) ? current : [] | ||
| }) | ||
| const leafRawMap = computed(() => { | ||
| const map = new Map<string, unknown>() | ||
| for (const row of leafRows.value) { | ||
| map.set(row.value, row.rawValue) | ||
| } | ||
| return map | ||
| }) | ||
| const multipleBindKeys = computed(() => | ||
| multipleModelValues.value.map((raw) => { | ||
| const hit = leafRows.value.find( | ||
| (row) => Object.is(row.rawValue, raw) || String(row.rawValue) === String(raw) | ||
| ) | ||
| return hit ? hit.value : String(raw) | ||
| }) | ||
| ) | ||
| const multipleSummary = computed(() => { | ||
| if (!multipleBindKeys.value.length) return '' | ||
| return multipleBindKeys.value | ||
| .map((key) => leafRows.value.find((row) => row.value === key)?.text ?? key) | ||
| .join('、') | ||
| }) | ||
| const useUniPicker = computed(() => !multiple.value && !emitPath.value) | ||
| const placeholderText = computed(() => String(cfg.value.placeholder || '请选择')) | ||
| const popupTitle = computed(() => String(cfg.value.popupTitle || cfg.value['popup-title'] || '请选择')) | ||
| function invokeConfigOnChange(value: unknown) { | ||
@@ -166,7 +246,2 @@ const callback = cfg.value.onChange as ((payload: unknown) => void) | undefined | ||
| function nodeChildren(node: Record<string, unknown> | undefined) { | ||
| const childRaw = node?.[childrenKey.value] | ||
| return Array.isArray(childRaw) && childRaw.length ? (childRaw as Record<string, unknown>[]) : null | ||
| } | ||
| function rebuildFromPath(prefix: number[]) { | ||
@@ -325,2 +400,11 @@ const tree = options.value | ||
| } | ||
| function onMultipleUpdate(keys: string[] | string) { | ||
| const list = Array.isArray(keys) ? keys : keys ? [keys] : [] | ||
| const rawList = list.map((key) => { | ||
| if (leafRawMap.value.has(key)) return leafRawMap.value.get(key) | ||
| return key | ||
| }) | ||
| emitValue(rawList) | ||
| } | ||
| </script> | ||
@@ -378,7 +462,12 @@ | ||
| } | ||
| .flmCascader-tip { | ||
| font-size: 24rpx; | ||
| color: #ee0a24; | ||
| padding: 16rpx 0; | ||
| .flmCascader-multiple { | ||
| width: 100%; | ||
| } | ||
| .flmCascader-multiple-summary { | ||
| display: block; | ||
| font-size: 26rpx; | ||
| color: #323233; | ||
| margin-bottom: 12rpx; | ||
| } | ||
| </style> |
| <template> | ||
| <view :id="idAttr" class="flm-date-picker"> | ||
| <!-- #ifdef H5 --> | ||
| <input v-if="nameAttr && !isRange" type="hidden" :name="nameAttr" :value="nameValueStr" /> | ||
| <input | ||
| v-if="nameAttr" | ||
| type="hidden" | ||
| :name="nameAttr" | ||
| :value="nameValueStr" | ||
| /> | ||
| <!-- #endif --> | ||
| <view v-if="isRange" class="flm-date-unsupported">日期范围请在后续版本使用</view> | ||
| <uni-datetime-picker | ||
| v-else | ||
| :key="pickerSyncKey" | ||
| :model-value="stringVal" | ||
| :type="pickerType" | ||
| :model-value="pickerBindValue" | ||
| :type="uniPickerType" | ||
| :start="startStr" | ||
@@ -18,7 +21,7 @@ :end="endStr" | ||
| :range-separator="rangeSeparatorStr" | ||
| :start-placeholder="startPlaceholderStr" | ||
| :end-placeholder="endPlaceholderStr" | ||
| return-type="string" | ||
| :start-placeholder="rangeStartPlaceholder" | ||
| :end-placeholder="rangeEndPlaceholder" | ||
| :return-type="returnTypeStr" | ||
| :hide-second="hideSecond" | ||
| :placeholder="placeholderText" | ||
| :placeholder="singlePlaceholderText" | ||
| :default-value="calendarOpenAnchor()" | ||
@@ -81,5 +84,48 @@ @show="onPickerShow" | ||
| function normalizeRangePart(raw: unknown, withDateTime: boolean): string { | ||
| if (raw == null || raw === '') return '' | ||
| if (typeof raw === 'string') return raw.trim() | ||
| if (typeof raw === 'number' && Number.isFinite(raw)) { | ||
| const d = new Date(raw) | ||
| if (!Number.isNaN(d.getTime())) return withDateTime ? formatDateTime(d) : formatDate(d) | ||
| return '' | ||
| } | ||
| if (raw instanceof Date) return withDateTime ? formatDateTime(raw) : formatDate(raw) | ||
| return '' | ||
| } | ||
| function parseRangeModelValue( | ||
| modelValue: string | number | Date | Record<string, unknown> | unknown[] | undefined, | ||
| withDateTime: boolean | ||
| ): string[] { | ||
| if (Array.isArray(modelValue)) { | ||
| return [ | ||
| normalizeRangePart(modelValue[0], withDateTime), | ||
| normalizeRangePart(modelValue[1], withDateTime) | ||
| ] | ||
| } | ||
| if (typeof modelValue === 'string' && modelValue.includes(',')) { | ||
| const parts = modelValue.split(',').map((s) => s.trim()) | ||
| return [ | ||
| normalizeRangePart(parts[0], withDateTime), | ||
| normalizeRangePart(parts[1], withDateTime) | ||
| ] | ||
| } | ||
| return ['', ''] | ||
| } | ||
| function toMonthOnly(value: string): string { | ||
| const head = String(value).trim().split(/\s+/)[0] ?? '' | ||
| if (!head) return '' | ||
| const segs = head.split(/[-/]/) | ||
| if (segs.length >= 2) { | ||
| const monthNum = Number(segs[1]) | ||
| return `${segs[0]}-${Number.isFinite(monthNum) ? pad(monthNum) : segs[1]}` | ||
| } | ||
| return head.slice(0, 7) | ||
| } | ||
| const props = withDefaults( | ||
| defineProps<{ | ||
| modelValue?: string | number | Date | Record<string, unknown> | ||
| modelValue?: string | number | Date | Record<string, unknown> | unknown[] | ||
| config?: DatePickerConfig | ||
@@ -94,4 +140,4 @@ }>(), | ||
| const emit = defineEmits<{ | ||
| 'update:modelValue': [string | number] | ||
| change: [string | number] | ||
| 'update:modelValue': [string | number | unknown[]] | ||
| change: [string | number | unknown[]] | ||
| 'visible-change': [boolean] | ||
@@ -108,5 +154,14 @@ }>() | ||
| const pickerType = computed(() => { | ||
| const isMonthRange = computed(() => String(dateType.value) === 'monthrange') | ||
| const hasDateTime = computed(() => { | ||
| const t = String(dateType.value) | ||
| if (t === 'datetime' || t === 'datetimerange') return 'datetime' | ||
| return t === 'datetime' || t === 'datetimerange' | ||
| }) | ||
| const uniPickerType = computed(() => { | ||
| const t = String(dateType.value) | ||
| if (t === 'datetimerange') return 'datetimerange' | ||
| if (t === 'daterange' || t === 'monthrange') return 'daterange' | ||
| if (t === 'datetime') return 'datetime' | ||
| if (t === 'time') return 'time' | ||
@@ -116,2 +171,4 @@ return 'date' | ||
| const returnTypeStr = computed(() => (isRange.value ? 'array' : 'string')) | ||
| const startStr = computed(() => { | ||
@@ -136,11 +193,7 @@ const c = cfg.value | ||
| const borderEnabled = computed(() => { | ||
| if (cfg.value.border === false) return false | ||
| return true | ||
| }) | ||
| const borderEnabled = computed(() => cfg.value.border !== false) | ||
| const clearIconEnabled = computed(() => { | ||
| const clearableFlag: unknown = cfg.value.clearable ?? cfg.value['clearable'] | ||
| if (clearableFlag === false) return false | ||
| return true | ||
| return clearableFlag !== false | ||
| }) | ||
@@ -152,10 +205,18 @@ | ||
| const startPlaceholderStr = computed(() => | ||
| String(cfg.value['start-placeholder'] ?? cfg.value.startPlaceholder ?? '') | ||
| ) | ||
| const rangeStartPlaceholder = computed(() => { | ||
| const explicit = cfg.value['start-placeholder'] ?? cfg.value.startPlaceholder | ||
| if (explicit != null && String(explicit).trim() !== '') return String(explicit) | ||
| if (isRange.value) return '开始日期' | ||
| return '' | ||
| }) | ||
| const endPlaceholderStr = computed(() => | ||
| String(cfg.value['end-placeholder'] ?? cfg.value.endPlaceholder ?? '') | ||
| ) | ||
| const rangeEndPlaceholder = computed(() => { | ||
| const explicit = cfg.value['end-placeholder'] ?? cfg.value.endPlaceholder | ||
| if (explicit != null && String(explicit).trim() !== '') return String(explicit) | ||
| if (isRange.value) return '结束日期' | ||
| return '' | ||
| }) | ||
| const singlePlaceholderText = computed(() => String(cfg.value.placeholder || '请选择日期')) | ||
| const hideSecond = computed(() => { | ||
@@ -166,4 +227,2 @@ const fmt = cfg.value.valueFormat || cfg.value['value-format'] | ||
| const placeholderText = computed(() => String(cfg.value.placeholder || '请选择日期')) | ||
| const idAttr = computed(() => { | ||
@@ -180,15 +239,21 @@ const id = cfg.value.id | ||
| const stringVal = computed(() => { | ||
| const pickerBindValue = computed(() => { | ||
| if (isRange.value) { | ||
| const pair = parseRangeModelValue(props.modelValue, hasDateTime.value) | ||
| const normalized = isMonthRange.value | ||
| ? [toMonthOnly(pair[0]), toMonthOnly(pair[1])] | ||
| : pair | ||
| if (!normalized[0] && !normalized[1]) return [] | ||
| return normalized | ||
| } | ||
| const v = props.modelValue | ||
| if (v == null || v === '') return '' | ||
| if (Array.isArray(v)) return '' | ||
| if (typeof v === 'string') return v | ||
| if (typeof v === 'number' && Number.isFinite(v)) { | ||
| const d = new Date(v) | ||
| if (!Number.isNaN(d.getTime())) | ||
| return pickerType.value === 'datetime' ? formatDateTime(d) : formatDate(d) | ||
| if (!Number.isNaN(d.getTime())) return hasDateTime.value ? formatDateTime(d) : formatDate(d) | ||
| return String(v) | ||
| } | ||
| if (v instanceof Date) { | ||
| return pickerType.value === 'datetime' ? formatDateTime(v) : formatDate(v) | ||
| } | ||
| if (v instanceof Date) return hasDateTime.value ? formatDateTime(v) : formatDate(v) | ||
| return '' | ||
@@ -198,8 +263,10 @@ }) | ||
| function calendarOpenAnchor(): string | undefined { | ||
| if (pickerType.value === 'time') return undefined | ||
| if (stringVal.value !== '') return undefined | ||
| if (isRange.value) return undefined | ||
| const bind = pickerBindValue.value | ||
| if (bind !== '' && bind != null) return undefined | ||
| if (uniPickerType.value === 'time') return undefined | ||
| const minStr = startStr.value | ||
| const maxStr = endStr.value | ||
| const anchor = clampDayToRange(todayLocalMidnight(), minStr, maxStr) | ||
| if (pickerType.value === 'datetime') { | ||
| if (hasDateTime.value) { | ||
| const base = formatDate(anchor) | ||
@@ -217,5 +284,13 @@ return base ? `${base} 00:00` : undefined | ||
| const pickerSyncKey = computed(() => `${fieldKeyPrefix.value}__${stringVal.value || 'empty'}`) | ||
| const pickerSyncKey = computed(() => { | ||
| const bind = pickerBindValue.value | ||
| const suffix = Array.isArray(bind) ? bind.join('|') : bind || 'empty' | ||
| return `${fieldKeyPrefix.value}__${suffix}` | ||
| }) | ||
| const nameValueStr = computed(() => stringVal.value) | ||
| const nameValueStr = computed(() => { | ||
| const bind = pickerBindValue.value | ||
| if (Array.isArray(bind)) return bind.join(',') | ||
| return String(bind ?? '') | ||
| }) | ||
@@ -229,5 +304,28 @@ function applyFormat(val: string, fmt: string) { | ||
| function onUpdate(val: string) { | ||
| function applyFormatToRange(pair: string[], fmt: string | undefined) { | ||
| if (!fmt) return pair | ||
| return pair.map((part) => (part ? applyFormat(part, String(fmt)) : part)) | ||
| } | ||
| function onUpdate(val: string | string[]) { | ||
| const fmt = cfg.value.valueFormat || cfg.value['value-format'] | ||
| const out = (fmt ? applyFormat(val || '', String(fmt)) : val) as string | number | ||
| if (isRange.value) { | ||
| let pair: string[] = [] | ||
| if (Array.isArray(val)) { | ||
| pair = val.map((part) => String(part ?? '')) | ||
| } else if (typeof val === 'string' && val) { | ||
| pair = [val, ''] | ||
| } | ||
| if (isMonthRange.value) { | ||
| pair = pair.map((part) => toMonthOnly(part)) | ||
| } else if (fmt) { | ||
| pair = applyFormatToRange(pair, String(fmt)) | ||
| } | ||
| while (pair.length < 2) pair.push('') | ||
| emit('update:modelValue', pair) | ||
| emit('change', pair) | ||
| return | ||
| } | ||
| const raw = typeof val === 'string' ? val : '' | ||
| const out = (fmt ? applyFormat(raw || '', String(fmt)) : raw) as string | number | ||
| emit('update:modelValue', out) | ||
@@ -250,8 +348,2 @@ emit('change', out) | ||
| } | ||
| .flm-date-unsupported { | ||
| font-size: 24rpx; | ||
| color: #ee0a24; | ||
| padding: 16rpx 0; | ||
| } | ||
| </style> |
@@ -5,4 +5,10 @@ <template> | ||
| <view class="flm-control"> | ||
| <flm-alert v-if="isAlertType" :config="alertConfig" /> | ||
| <flm-read | ||
| v-else-if="readAsTextMode" | ||
| :model-value="readDisplayValue" | ||
| :config="readDisplayConfig" | ||
| /> | ||
| <flm-input | ||
| v-if="matchType(formItem, 'flmInput')" | ||
| v-else-if="matchType(formItem, 'flmInput')" | ||
| :model-value="coercedValue" | ||
@@ -138,2 +144,3 @@ :config="mergedConfig" | ||
| import type { DynamicFormItemConfig } from 'flame-uni-types' | ||
| import FlmAlert from '../flmAlert/flmAlert.vue' | ||
| import FlmCascader from '../flmCascader/flmCascader.vue' | ||
@@ -162,2 +169,4 @@ import FlmCheckbox from '../flmCheckbox/flmCheckbox.vue' | ||
| type FormMode = 'edit' | 'read' | 'disabled' | ||
| const props = withDefaults( | ||
@@ -168,2 +177,3 @@ defineProps<{ | ||
| isEdit?: boolean | ||
| mode?: FormMode | ||
| noLabel?: boolean | ||
@@ -175,2 +185,3 @@ }>(), | ||
| isEdit: true, | ||
| mode: 'edit', | ||
| noLabel: false | ||
@@ -186,8 +197,15 @@ } | ||
| const effectiveMode = computed((): FormMode => { | ||
| if (props.isEdit === false) return 'read' | ||
| const mode = props.mode | ||
| if (mode === 'read' || mode === 'disabled') return mode | ||
| return 'edit' | ||
| }) | ||
| function controlTypeOf(item: DynamicFormItemConfig) { | ||
| const t = item.controlType | ||
| if (t == null || t === '') return 'flmInput' | ||
| const s = String(t) | ||
| if (s === 'FlmEditor') return 'flmEditor' | ||
| return s | ||
| const controlType = item.controlType | ||
| if (controlType == null || controlType === '') return 'flmInput' | ||
| const typeString = String(controlType) | ||
| if (typeString === 'FlmEditor') return 'flmEditor' | ||
| return typeString | ||
| } | ||
@@ -199,30 +217,47 @@ | ||
| const isAlertType = computed(() => matchType(formItem.value, 'flmAlert')) | ||
| const alertConfig = computed(() => { | ||
| const controlConfig = formItem.value.controlConfig | ||
| return controlConfig && typeof controlConfig === 'object' | ||
| ? (controlConfig as Record<string, unknown>) | ||
| : {} | ||
| }) | ||
| const readAsTextMode = computed( | ||
| () => | ||
| effectiveMode.value === 'read' && | ||
| !isAlertType.value && | ||
| !matchType(formItem.value, 'flmRead') && | ||
| !matchType(formItem.value, 'flmImage') | ||
| ) | ||
| function defaultForItem(item: DynamicFormItemConfig) { | ||
| const t = controlTypeOf(item) | ||
| const cc = item.controlConfig || {} | ||
| if (t === 'flmSwitch') { | ||
| if (cc.inactiveValue !== undefined) return cc.inactiveValue | ||
| if (cc['inactive-value'] !== undefined) return cc['inactive-value'] | ||
| const controlType = controlTypeOf(item) | ||
| const controlConfig = item.controlConfig || {} | ||
| if (controlType === 'flmSwitch') { | ||
| if (controlConfig.inactiveValue !== undefined) return controlConfig.inactiveValue | ||
| if (controlConfig['inactive-value'] !== undefined) return controlConfig['inactive-value'] | ||
| return false | ||
| } | ||
| if (t === 'flmTime') return '' | ||
| if (t === 'flmCheckbox') { | ||
| if (controlType === 'flmTime') return '' | ||
| if (controlType === 'flmCheckbox') { | ||
| if (isFlmCheckboxSingleModeForFormItem(item)) return false | ||
| return [] | ||
| } | ||
| if (t === 'flmCheckboxGroup') return [] | ||
| if (t === 'flmInputNumber') return cc.min !== undefined ? Number(cc.min) : 0 | ||
| if (t === 'flmSlider') return cc.min !== undefined ? Number(cc.min) : 0 | ||
| if (t === 'flmRate') return 0 | ||
| if (t === 'flmCascader') { | ||
| const cascaderProps = cc.props as { emitPath?: boolean } | undefined | ||
| if (controlType === 'flmCheckboxGroup') return [] | ||
| if (controlType === 'flmInputNumber') return controlConfig.min !== undefined ? Number(controlConfig.min) : 0 | ||
| if (controlType === 'flmSlider') return controlConfig.min !== undefined ? Number(controlConfig.min) : 0 | ||
| if (controlType === 'flmRate') return 0 | ||
| if (controlType === 'flmCascader') { | ||
| const cascaderProps = controlConfig.props as { emitPath?: boolean } | undefined | ||
| return cascaderProps?.emitPath ? [] : '' | ||
| } | ||
| if (t === 'flmColorPicker') return '' | ||
| if (t === 'flmUpload') return [] | ||
| if (t === 'flmImage') return '' | ||
| if (t === 'flmSearchSelect') return '' | ||
| if (t === 'flmTimeSelect') return '' | ||
| if (t === 'flmTransfer') return [] | ||
| if (t === 'flmEditor') return '' | ||
| if (controlType === 'flmColorPicker') return '' | ||
| if (controlType === 'flmUpload') return [] | ||
| if (controlType === 'flmImage') return '' | ||
| if (controlType === 'flmSearchSelect') return '' | ||
| if (controlType === 'flmTimeSelect') return '' | ||
| if (controlType === 'flmTransfer') return [] | ||
| if (controlType === 'flmEditor') return '' | ||
| return '' | ||
@@ -233,7 +268,9 @@ } | ||
| const formItemRecord = props.item || {} | ||
| const cc = | ||
| const controlConfig = | ||
| formItemRecord.controlConfig && typeof formItemRecord.controlConfig === 'object' | ||
| ? { ...(formItemRecord.controlConfig as Record<string, unknown>) } | ||
| : {} | ||
| if (!props.isEdit) cc.disabled = true | ||
| if (effectiveMode.value !== 'edit') { | ||
| controlConfig.disabled = true | ||
| } | ||
| if ( | ||
@@ -243,3 +280,3 @@ controlTypeOf(formItemRecord) === 'flmCheckbox' && | ||
| ) { | ||
| const controlLabelRaw = cc.label | ||
| const controlLabelRaw = controlConfig.label | ||
| const controlLabelTrimmed = | ||
@@ -258,44 +295,60 @@ controlLabelRaw != null && String(controlLabelRaw).trim() !== '' | ||
| if (titleFromItem != null && String(titleFromItem).trim() !== '') { | ||
| cc.fieldDisplayLabel = String(titleFromItem) | ||
| controlConfig.fieldDisplayLabel = String(titleFromItem) | ||
| } | ||
| } | ||
| } | ||
| return cc | ||
| return controlConfig | ||
| }) | ||
| const coercedValue = computed(() => { | ||
| const it = props.item | ||
| const v = props.modelValue | ||
| const t = controlTypeOf(it) | ||
| if (t === 'flmCheckbox') { | ||
| if (isFlmCheckboxSingleModeForFormItem(it)) { | ||
| if (v === undefined || v === null) return defaultForItem(it) | ||
| if (Array.isArray(v)) { | ||
| if (v.length === 0) return defaultForItem(it) | ||
| if (v.length === 1) return v[0] | ||
| return defaultForItem(it) | ||
| const item = props.item | ||
| const value = props.modelValue | ||
| const controlType = controlTypeOf(item) | ||
| if (controlType === 'flmCheckbox') { | ||
| if (isFlmCheckboxSingleModeForFormItem(item)) { | ||
| if (value === undefined || value === null) return defaultForItem(item) | ||
| if (Array.isArray(value)) { | ||
| if (value.length === 0) return defaultForItem(item) | ||
| if (value.length === 1) return value[0] | ||
| return defaultForItem(item) | ||
| } | ||
| return v | ||
| return value | ||
| } | ||
| if (v === undefined || v === null) return defaultForItem(it) | ||
| return Array.isArray(v) ? v : [] | ||
| if (value === undefined || value === null) return defaultForItem(item) | ||
| return Array.isArray(value) ? value : [] | ||
| } | ||
| if (t === 'flmCheckboxGroup' || t === 'flmUpload' || t === 'flmTransfer') { | ||
| if (v === undefined || v === null) return defaultForItem(it) | ||
| return Array.isArray(v) ? v : [] | ||
| if (controlType === 'flmCheckboxGroup' || controlType === 'flmUpload' || controlType === 'flmTransfer') { | ||
| if (value === undefined || value === null) return defaultForItem(item) | ||
| return Array.isArray(value) ? value : [] | ||
| } | ||
| if (t === 'flmTime') { | ||
| if (v === undefined || v === null) return '' | ||
| return String(v) | ||
| if (controlType === 'flmTime') { | ||
| if (value === undefined || value === null) return '' | ||
| return String(value) | ||
| } | ||
| if (t === 'flmInputNumber' || t === 'flmSlider' || t === 'flmRate') { | ||
| if (v === undefined || v === null) return defaultForItem(it) | ||
| const n = Number(v) | ||
| return Number.isNaN(n) ? defaultForItem(it) : n | ||
| if (controlType === 'flmInputNumber' || controlType === 'flmSlider' || controlType === 'flmRate') { | ||
| if (value === undefined || value === null) return defaultForItem(item) | ||
| const numberValue = Number(value) | ||
| return Number.isNaN(numberValue) ? defaultForItem(item) : numberValue | ||
| } | ||
| if (v === undefined || v === null) return defaultForItem(it) | ||
| return v | ||
| if (value === undefined || value === null) return defaultForItem(item) | ||
| return value | ||
| }) | ||
| function formatReadText(value: unknown) { | ||
| if (value === undefined || value === null || value === '') return '—' | ||
| if (typeof value === 'boolean') return value ? '是' : '否' | ||
| if (Array.isArray(value)) return value.map((entry) => String(entry)).join('、') | ||
| if (typeof value === 'object') return JSON.stringify(value) | ||
| return String(value) | ||
| } | ||
| const readDisplayValue = computed(() => formatReadText(coercedValue.value)) | ||
| const readDisplayConfig = computed(() => ({ | ||
| ...mergedConfig.value, | ||
| 'model-value': readDisplayValue.value | ||
| })) | ||
| function emitVal(value: unknown) { | ||
| if (effectiveMode.value !== 'edit') return | ||
| emit('update:modelValue', value) | ||
@@ -302,0 +355,0 @@ } |
@@ -55,3 +55,3 @@ <template> | ||
| color: '#323233', | ||
| disableColor: '#323233', | ||
| disableColor: 'transparent', | ||
| borderColor: 'transparent', | ||
@@ -66,2 +66,10 @@ backgroundColor: 'transparent' | ||
| } | ||
| .flmRead-wrap ::v-deep(.uni-easyinput__content.is-disabled) { | ||
| background-color: transparent !important; | ||
| color: #323233; | ||
| } | ||
| .flmRead-wrap ::v-deep(.uni-easyinput__content-input) { | ||
| color: #323233 !important; | ||
| -webkit-text-fill-color: #323233; | ||
| } | ||
| </style> |
@@ -6,35 +6,65 @@ <template> | ||
| <!-- #endif --> | ||
| <view v-if="filterableEnabled" class="flm-select-search"> | ||
| <uni-easyinput | ||
| :model-value="filterQuery" | ||
| type="text" | ||
| :placeholder="filterPlaceholder" | ||
| <template v-if="multipleEnabled"> | ||
| <text v-if="multipleSummary" class="flm-select-multiple-summary">{{ multipleSummary }}</text> | ||
| <view v-if="filterableEnabled" class="flm-select-search"> | ||
| <uni-easyinput | ||
| :model-value="filterQuery" | ||
| type="text" | ||
| :placeholder="filterPlaceholder" | ||
| :disabled="pickerReadonly" | ||
| trim="none" | ||
| :input-border="true" | ||
| :clearable="false" | ||
| primary-color="#2979ff" | ||
| @update:model-value="onFilterInput" | ||
| /> | ||
| </view> | ||
| <view v-if="showNoHit" class="flm-select-empty-hint">{{ noHitText }}</view> | ||
| <view v-if="showLoading" class="flm-select-loading-mask"> | ||
| <text class="flm-select-loading-text">{{ loadingText }}</text> | ||
| </view> | ||
| <uni-data-checkbox | ||
| multiple | ||
| mode="list" | ||
| :localdata="localdataForCheckbox" | ||
| :model-value="multipleBindKeys" | ||
| :disabled="pickerReadonly" | ||
| trim="none" | ||
| :input-border="true" | ||
| :clearable="false" | ||
| primary-color="#2979ff" | ||
| @update:model-value="onFilterInput" | ||
| @update:model-value="onMultipleUpdate" | ||
| /> | ||
| </view> | ||
| <view v-if="showNoHit" class="flm-select-empty-hint">{{ noHitText }}</view> | ||
| <view v-if="showLoading" class="flm-select-loading-mask"> | ||
| <text class="flm-select-loading-text">{{ loadingText }}</text> | ||
| </view> | ||
| <view v-if="allowCreateEnabled && createHintVisible" class="flm-select-create-row"> | ||
| <text class="flm-select-create-btn" @click="emitCreate">{{ createBtnLabel }}</text> | ||
| </view> | ||
| <uni-data-picker | ||
| :model-value="pickerBindValue" | ||
| :localdata="localdataForPicker" | ||
| :placeholder="placeholderText" | ||
| :popup-title="popupTitleText" | ||
| :readonly="pickerReadonly" | ||
| :clear-icon="clearable" | ||
| :border="borderEnabled" | ||
| :step-searh="false" | ||
| @popupopened="onPopupOpened" | ||
| @popupclosed="onPopupClosed" | ||
| @update:model-value="onUpdate" | ||
| /> | ||
| </template> | ||
| <template v-else> | ||
| <view v-if="filterableEnabled" class="flm-select-search"> | ||
| <uni-easyinput | ||
| :model-value="filterQuery" | ||
| type="text" | ||
| :placeholder="filterPlaceholder" | ||
| :disabled="pickerReadonly" | ||
| trim="none" | ||
| :input-border="true" | ||
| :clearable="false" | ||
| primary-color="#2979ff" | ||
| @update:model-value="onFilterInput" | ||
| /> | ||
| </view> | ||
| <view v-if="showNoHit" class="flm-select-empty-hint">{{ noHitText }}</view> | ||
| <view v-if="showLoading" class="flm-select-loading-mask"> | ||
| <text class="flm-select-loading-text">{{ loadingText }}</text> | ||
| </view> | ||
| <view v-if="allowCreateEnabled && createHintVisible" class="flm-select-create-row"> | ||
| <text class="flm-select-create-btn" @click="emitCreate">{{ createBtnLabel }}</text> | ||
| </view> | ||
| <uni-data-picker | ||
| :model-value="pickerBindValue" | ||
| :localdata="localdataForPicker" | ||
| :placeholder="placeholderText" | ||
| :popup-title="popupTitleText" | ||
| :readonly="pickerReadonly" | ||
| :clear-icon="clearable" | ||
| :border="borderEnabled" | ||
| :step-searh="false" | ||
| @popupopened="onPopupOpened" | ||
| @popupclosed="onPopupClosed" | ||
| @update:model-value="onUpdate" | ||
| /> | ||
| </template> | ||
| </view> | ||
@@ -70,9 +100,13 @@ </template> | ||
| const clearable = computed(() => cfg.value.clearable === true) | ||
| const multipleEnabled = computed(() => cfg.value.multiple === true) | ||
| const borderEnabled = computed(() => { | ||
| if (cfg.value.border === false) return false | ||
| return true | ||
| const multipleLimit = computed(() => { | ||
| const limit = cfg.value['multiple-limit'] ?? cfg.value.multipleLimit | ||
| return typeof limit === 'number' && limit > 0 ? limit : 0 | ||
| }) | ||
| const clearable = computed(() => cfg.value.clearable === true) | ||
| const borderEnabled = computed(() => cfg.value.border !== false) | ||
| const placeholderText = computed(() => String(cfg.value.placeholder || '请选择')) | ||
@@ -95,8 +129,16 @@ | ||
| const c = cfg.value | ||
| return c['model-value'] ?? c.modelValue ?? '' | ||
| const fallback = c['model-value'] ?? c.modelValue | ||
| if (fallback !== undefined && fallback !== null) return fallback | ||
| return multipleEnabled.value ? [] : '' | ||
| }) | ||
| const multipleModelValues = computed(() => { | ||
| const v = effectiveModelValue.value | ||
| return Array.isArray(v) ? v : [] | ||
| }) | ||
| const nameInputValue = computed(() => { | ||
| const v = effectiveModelValue.value | ||
| if (v === null || v === undefined) return '' | ||
| if (Array.isArray(v)) return v.map((item) => String(item)).join(',') | ||
| if (typeof v === 'object' && !Array.isArray(v)) return JSON.stringify(v) | ||
@@ -167,8 +209,8 @@ return String(v) | ||
| function optionPrimitive(o: Record<string, unknown>): string | number | boolean { | ||
| const vk = valueKeyStr.value | ||
| const val = o.value | ||
| function optionPrimitive(optionRow: Record<string, unknown>): string | number | boolean { | ||
| const valueKey = valueKeyStr.value | ||
| const val = optionRow.value | ||
| if (val !== null && typeof val === 'object' && !Array.isArray(val)) { | ||
| const obj = val as Record<string, unknown> | ||
| if (vk in obj) return obj[vk] as string | number | boolean | ||
| if (valueKey in obj) return obj[valueKey] as string | number | boolean | ||
| return JSON.stringify(val) | ||
@@ -179,10 +221,10 @@ } | ||
| function optionRowFromRaw(o: Record<string, unknown>) { | ||
| const pv = optionPrimitive(o) | ||
| const rawValue = o.value | ||
| function optionRowFromRaw(optionRow: Record<string, unknown>) { | ||
| const pickerValue = optionPrimitive(optionRow) | ||
| const rawValue = optionRow.value | ||
| return { | ||
| text: o.label != null ? String(o.label) : String(o.value ?? ''), | ||
| pickerValue: pv, | ||
| text: optionRow.label != null ? String(optionRow.label) : String(optionRow.value ?? ''), | ||
| pickerValue, | ||
| rawValue, | ||
| disable: Boolean(o.disabled) | ||
| disable: Boolean(optionRow.disabled) | ||
| } | ||
@@ -195,9 +237,9 @@ } | ||
| const out: Record<string, unknown>[] = [] | ||
| for (const g of c.groups as Record<string, unknown>[]) { | ||
| const gl = g.label != null ? String(g.label) : '' | ||
| const opts = Array.isArray(g.options) ? (g.options as Record<string, unknown>[]) : [] | ||
| for (const o of opts) { | ||
| const row = optionRowFromRaw(o) | ||
| for (const group of c.groups as Record<string, unknown>[]) { | ||
| const groupLabel = group.label != null ? String(group.label) : '' | ||
| const opts = Array.isArray(group.options) ? (group.options as Record<string, unknown>[]) : [] | ||
| for (const option of opts) { | ||
| const row = optionRowFromRaw(option) | ||
| out.push({ | ||
| label: gl ? `${gl} / ${row.text}` : row.text, | ||
| label: groupLabel ? `${groupLabel} / ${row.text}` : row.text, | ||
| value: row.rawValue, | ||
@@ -230,4 +272,4 @@ disabled: row.disable | ||
| try { | ||
| const r = (fn as (q: string) => unknown)(filterQuery.value) | ||
| if (Array.isArray(r)) return (r as Record<string, unknown>[]).map(optionRowFromRaw) | ||
| const result = (fn as (q: string) => unknown)(filterQuery.value) | ||
| if (Array.isArray(result)) return (result as Record<string, unknown>[]).map(optionRowFromRaw) | ||
| } catch { | ||
@@ -237,12 +279,12 @@ return rows | ||
| } | ||
| const q = filterQuery.value.trim().toLowerCase() | ||
| return rows.filter((row) => row.text.toLowerCase().includes(q)) | ||
| const query = filterQuery.value.trim().toLowerCase() | ||
| return rows.filter((row) => row.text.toLowerCase().includes(query)) | ||
| }) | ||
| const rawValueMap = computed(() => { | ||
| const m = new Map<string, unknown>() | ||
| const map = new Map<string, unknown>() | ||
| for (const row of optionsAfterRemote.value) { | ||
| m.set(String(row.pickerValue), row.rawValue) | ||
| map.set(String(row.pickerValue), row.rawValue) | ||
| } | ||
| return m | ||
| return map | ||
| }) | ||
@@ -258,9 +300,40 @@ | ||
| const localdataForCheckbox = computed(() => | ||
| optionsAfterFilter.value.map((row) => ({ | ||
| text: row.text, | ||
| value: String(row.pickerValue), | ||
| disable: row.disable | ||
| })) | ||
| ) | ||
| const multipleBindKeys = computed(() => | ||
| multipleModelValues.value.map((raw) => { | ||
| const hit = optionsAfterRemote.value.find((row) => { | ||
| const mapped = rawValueMap.value.get(String(row.pickerValue)) | ||
| if (mapped !== undefined && Object.is(mapped, raw)) return true | ||
| return Object.is(row.rawValue, raw) || String(row.pickerValue) === String(raw) | ||
| }) | ||
| return hit ? String(hit.pickerValue) : String(raw) | ||
| }) | ||
| ) | ||
| const multipleSummary = computed(() => { | ||
| if (!multipleBindKeys.value.length) return '' | ||
| const labels = multipleBindKeys.value | ||
| .map( | ||
| (key) => | ||
| optionsAfterRemote.value.find((row) => String(row.pickerValue) === key)?.text ?? key | ||
| ) | ||
| .filter(Boolean) | ||
| return labels.join('、') | ||
| }) | ||
| const pickerBindValue = computed(() => { | ||
| const v = effectiveModelValue.value | ||
| if (v === null || v === undefined || v === '') return '' | ||
| if (typeof v === 'object' && !Array.isArray(v)) { | ||
| const vk = valueKeyStr.value | ||
| if (vk in (v as Record<string, unknown>)) | ||
| return (v as Record<string, unknown>)[vk] as string | number | ||
| if (Array.isArray(v)) return '' | ||
| if (typeof v === 'object') { | ||
| const valueKey = valueKeyStr.value | ||
| if (valueKey in (v as Record<string, unknown>)) | ||
| return (v as Record<string, unknown>)[valueKey] as string | number | ||
| return JSON.stringify(v) | ||
@@ -275,3 +348,5 @@ } | ||
| filterQuery.value.trim().length > 0 && | ||
| localdataForPicker.value.length === 0 | ||
| (multipleEnabled.value | ||
| ? localdataForCheckbox.value.length === 0 | ||
| : localdataForPicker.value.length === 0) | ||
| ) | ||
@@ -287,5 +362,5 @@ | ||
| if (!allowCreateEnabled.value || !filterQuery.value.trim()) return false | ||
| const q = filterQuery.value.trim() | ||
| const query = filterQuery.value.trim() | ||
| return !optionsAfterRemote.value.some( | ||
| (row) => String(row.text) === q || String(row.pickerValue) === q | ||
| (row) => String(row.text) === query || String(row.pickerValue) === query | ||
| ) | ||
@@ -347,3 +422,3 @@ }) | ||
| ) { | ||
| const first = optionsAfterRemote.value.find((r) => !r.disable) | ||
| const first = optionsAfterRemote.value.find((row) => !row.disable) | ||
| if (first) { | ||
@@ -366,2 +441,16 @@ didDefaultFirst.value = true | ||
| function onMultipleUpdate(keys: string[] | string) { | ||
| const list = Array.isArray(keys) ? keys : keys ? [keys] : [] | ||
| const limited = | ||
| multipleLimit.value > 0 && list.length > multipleLimit.value | ||
| ? list.slice(0, multipleLimit.value) | ||
| : list | ||
| const rawList = limited.map((key) => { | ||
| if (rawValueMap.value.has(key)) return rawValueMap.value.get(key) | ||
| const hit = optionsAfterRemote.value.find((row) => String(row.pickerValue) === key) | ||
| return hit ? hit.rawValue : key | ||
| }) | ||
| emitValue(rawList) | ||
| } | ||
| function onUpdate(val: unknown) { | ||
@@ -378,3 +467,3 @@ if (val === '' || val === undefined || val === null) { | ||
| const hit = optionsAfterFilter.value.find( | ||
| (o) => !o.disable && String(o.pickerValue) === String(val) | ||
| (row) => !row.disable && String(row.pickerValue) === String(val) | ||
| ) | ||
@@ -389,5 +478,5 @@ if (hit) { | ||
| function emitCreate() { | ||
| const q = filterQuery.value.trim() | ||
| if (!q) return | ||
| emitValue(q) | ||
| const query = filterQuery.value.trim() | ||
| if (!query) return | ||
| emitValue(query) | ||
| } | ||
@@ -416,2 +505,6 @@ | ||
| }) | ||
| watch(multipleEnabled, () => { | ||
| if (remoteEnabled.value) scheduleRemote(filterQuery.value) | ||
| }) | ||
| </script> | ||
@@ -425,2 +518,9 @@ | ||
| .flm-select-multiple-summary { | ||
| display: block; | ||
| font-size: 26rpx; | ||
| color: #323233; | ||
| margin-bottom: 12rpx; | ||
| } | ||
| .flm-select-search { | ||
@@ -427,0 +527,0 @@ margin-bottom: 12rpx; |
| <template> | ||
| <view v-if="isRange" class="flmSlider-tip">双滑块范围请在后续版本使用</view> | ||
| <view v-if="isRange" class="flmSlider-range-wrap"> | ||
| <view class="flmSlider-range-row"> | ||
| <text class="flmSlider-range-label">起</text> | ||
| <view class="flmSlider-track" :style="trackInsetStyle"> | ||
| <slider | ||
| class="flmSlider-native" | ||
| style="width: 100%" | ||
| :value="rangeStart" | ||
| :min="minV" | ||
| :max="maxV" | ||
| :step="stepV" | ||
| :disabled="disabled" | ||
| :active-color="activeColorStr" | ||
| background-color="#e5e5e5" | ||
| :block-size="blockSizePx" | ||
| @changing="(e) => onRangeChanging(0, e)" | ||
| @change="(e) => onRangeChange(0, e)" | ||
| /> | ||
| </view> | ||
| <text v-if="showInput" class="flmSlider-val">{{ formatDisplay(rangeStart) }}</text> | ||
| </view> | ||
| <view class="flmSlider-range-row"> | ||
| <text class="flmSlider-range-label">止</text> | ||
| <view class="flmSlider-track" :style="trackInsetStyle"> | ||
| <slider | ||
| class="flmSlider-native" | ||
| style="width: 100%" | ||
| :value="rangeEnd" | ||
| :min="minV" | ||
| :max="maxV" | ||
| :step="stepV" | ||
| :disabled="disabled" | ||
| :active-color="activeColorStr" | ||
| background-color="#e5e5e5" | ||
| :block-size="blockSizePx" | ||
| @changing="(e) => onRangeChanging(1, e)" | ||
| @change="(e) => onRangeChange(1, e)" | ||
| /> | ||
| </view> | ||
| <text v-if="showInput" class="flmSlider-val">{{ formatDisplay(rangeEnd) }}</text> | ||
| </view> | ||
| </view> | ||
| <view v-else class="flmSlider-wrap"> | ||
@@ -61,5 +102,19 @@ <view class="flmSlider-track" :style="trackInsetStyle"> | ||
| function parseRangeModel( | ||
| modelValue: number | unknown[] | undefined, | ||
| minBound: number, | ||
| maxBound: number, | ||
| step: number | ||
| ): [number, number] { | ||
| if (Array.isArray(modelValue) && modelValue.length >= 2) { | ||
| const start = snapToStep(Number(modelValue[0]), minBound, maxBound, step) | ||
| const end = snapToStep(Number(modelValue[1]), minBound, maxBound, step) | ||
| return start <= end ? [start, end] : [end, start] | ||
| } | ||
| return [minBound, maxBound] | ||
| } | ||
| const props = withDefaults( | ||
| defineProps<{ | ||
| modelValue?: number | ||
| modelValue?: number | unknown[] | ||
| config?: SliderConfig | ||
@@ -74,5 +129,5 @@ }>(), | ||
| const emit = defineEmits<{ | ||
| 'update:modelValue': [number] | ||
| change: [number] | ||
| input: [number] | ||
| 'update:modelValue': [number | number[]] | ||
| change: [number | number[]] | ||
| input: [number | number[]] | ||
| }>() | ||
@@ -118,5 +173,6 @@ | ||
| const inner = ref(0) | ||
| const rangeStart = ref(0) | ||
| const rangeEnd = ref(100) | ||
| const displayValueText = computed(() => { | ||
| const value = inner.value | ||
| function formatDisplay(value: number) { | ||
| const places = decimalPlacesFromStep(stepV.value) | ||
@@ -126,7 +182,45 @@ if (!Number.isFinite(value)) return '0' | ||
| return String(Number(value.toFixed(places))) | ||
| }) | ||
| } | ||
| const displayValueText = computed(() => formatDisplay(inner.value)) | ||
| function invokeConfigChange(value: number | number[]) { | ||
| const callback = cfg.value.onChange as ((payload: unknown) => void) | undefined | ||
| if (typeof callback === 'function') callback(value) | ||
| } | ||
| function emitSingle(next: number) { | ||
| inner.value = next | ||
| emit('update:modelValue', next) | ||
| emit('change', next) | ||
| emit('input', next) | ||
| invokeConfigChange(next) | ||
| } | ||
| function emitRangePair(start: number, end: number) { | ||
| let lo = snapToStep(start, minV.value, maxV.value, stepV.value) | ||
| let hi = snapToStep(end, minV.value, maxV.value, stepV.value) | ||
| if (lo > hi) { | ||
| const swap = lo | ||
| lo = hi | ||
| hi = swap | ||
| } | ||
| rangeStart.value = lo | ||
| rangeEnd.value = hi | ||
| const pair: [number, number] = [lo, hi] | ||
| emit('update:modelValue', pair) | ||
| emit('change', pair) | ||
| emit('input', pair) | ||
| invokeConfigChange(pair) | ||
| } | ||
| watch( | ||
| () => props.modelValue, | ||
| (value) => { | ||
| if (isRange.value) { | ||
| const pair = parseRangeModel(value, minV.value, maxV.value, stepV.value) | ||
| rangeStart.value = pair[0] | ||
| rangeEnd.value = pair[1] | ||
| return | ||
| } | ||
| const raw = Number(value ?? merged.value['model-value']) | ||
@@ -140,4 +234,10 @@ const base = Number.isNaN(raw) ? minV.value : raw | ||
| watch( | ||
| () => [minV.value, maxV.value, stepV.value] as const, | ||
| () => [minV.value, maxV.value, stepV.value, isRange.value] as const, | ||
| () => { | ||
| if (isRange.value) { | ||
| const pair = parseRangeModel(props.modelValue, minV.value, maxV.value, stepV.value) | ||
| rangeStart.value = pair[0] | ||
| rangeEnd.value = pair[1] | ||
| return | ||
| } | ||
| inner.value = snapToStep(inner.value, minV.value, maxV.value, stepV.value) | ||
@@ -147,14 +247,5 @@ } | ||
| function invokeConfigChange(value: number) { | ||
| const callback = cfg.value.onChange as ((payload: unknown) => void) | undefined | ||
| if (typeof callback === 'function') callback(value) | ||
| } | ||
| function onChanging(event: { detail?: { value?: number } }) { | ||
| const raw = event.detail?.value ?? inner.value | ||
| const next = snapToStep(Number(raw), minV.value, maxV.value, stepV.value) | ||
| inner.value = next | ||
| emit('update:modelValue', next) | ||
| emit('input', next) | ||
| invokeConfigChange(next) | ||
| emitSingle(snapToStep(Number(raw), minV.value, maxV.value, stepV.value)) | ||
| } | ||
@@ -164,12 +255,45 @@ | ||
| const raw = event.detail?.value ?? 0 | ||
| const next = snapToStep(Number(raw), minV.value, maxV.value, stepV.value) | ||
| inner.value = next | ||
| emit('update:modelValue', next) | ||
| emit('change', next) | ||
| emit('input', next) | ||
| invokeConfigChange(next) | ||
| emitSingle(snapToStep(Number(raw), minV.value, maxV.value, stepV.value)) | ||
| } | ||
| function onRangeChanging(index: 0 | 1, event: { detail?: { value?: number } }) { | ||
| const raw = Number(event.detail?.value ?? (index === 0 ? rangeStart.value : rangeEnd.value)) | ||
| const next = snapToStep(raw, minV.value, maxV.value, stepV.value) | ||
| if (index === 0) emitRangePair(next, rangeEnd.value) | ||
| else emitRangePair(rangeStart.value, next) | ||
| } | ||
| function onRangeChange(index: 0 | 1, event: { detail?: { value?: number } }) { | ||
| onRangeChanging(index, event) | ||
| } | ||
| </script> | ||
| <style scoped> | ||
| .flmSlider-wrap, | ||
| .flmSlider-range-wrap { | ||
| width: 100%; | ||
| max-width: 100%; | ||
| box-sizing: border-box; | ||
| overflow-x: hidden; | ||
| } | ||
| .flmSlider-range-row { | ||
| display: flex; | ||
| flex-direction: row; | ||
| align-items: center; | ||
| gap: 12rpx; | ||
| margin-bottom: 16rpx; | ||
| } | ||
| .flmSlider-range-row:last-child { | ||
| margin-bottom: 0; | ||
| } | ||
| .flmSlider-range-label { | ||
| flex: 0 0 auto; | ||
| width: 40rpx; | ||
| font-size: 26rpx; | ||
| color: #646566; | ||
| } | ||
| .flmSlider-wrap { | ||
@@ -180,7 +304,4 @@ display: flex; | ||
| gap: 12rpx; | ||
| width: 100%; | ||
| max-width: 100%; | ||
| box-sizing: border-box; | ||
| overflow-x: hidden; | ||
| } | ||
| .flmSlider-track { | ||
@@ -191,5 +312,7 @@ flex: 1 1 0; | ||
| } | ||
| .flmSlider-native { | ||
| width: 100%; | ||
| } | ||
| .flmSlider-val { | ||
@@ -202,10 +325,2 @@ flex: 0 0 auto; | ||
| } | ||
| .flmSlider-tip { | ||
| font-size: 24rpx; | ||
| color: #ee0a24; | ||
| padding: 16rpx 0; | ||
| width: 100%; | ||
| max-width: 100%; | ||
| box-sizing: border-box; | ||
| } | ||
| </style> |
@@ -6,3 +6,33 @@ <template> | ||
| <!-- #endif --> | ||
| <view v-if="isRange" class="flm-time-unsupported">时间范围请在后续版本使用</view> | ||
| <view v-if="isRange" class="flm-time-range"> | ||
| <uni-datetime-picker | ||
| :key="`${pickerSyncKey}-start`" | ||
| :model-value="startPickerBind" | ||
| type="time" | ||
| return-type="string" | ||
| :disabled="fieldDisabled" | ||
| :border="borderEnabled" | ||
| :clear-icon="clearIconEnabled" | ||
| :hide-second="hideSecond" | ||
| :placeholder="startPlaceholderText" | ||
| @show="onPickerShow" | ||
| @mask-click="onPickerMaskClick" | ||
| @update:model-value="(v) => onRangePartUpdate(0, v)" | ||
| /> | ||
| <text class="flm-time-range-sep">{{ rangeSeparatorStr }}</text> | ||
| <uni-datetime-picker | ||
| :key="`${pickerSyncKey}-end`" | ||
| :model-value="endPickerBind" | ||
| type="time" | ||
| return-type="string" | ||
| :disabled="fieldDisabled" | ||
| :border="borderEnabled" | ||
| :clear-icon="clearIconEnabled" | ||
| :hide-second="hideSecond" | ||
| :placeholder="endPlaceholderText" | ||
| @show="onPickerShow" | ||
| @mask-click="onPickerMaskClick" | ||
| @update:model-value="(v) => onRangePartUpdate(1, v)" | ||
| /> | ||
| </view> | ||
| <uni-datetime-picker | ||
@@ -90,5 +120,25 @@ v-else | ||
| function parseRangeTimes( | ||
| modelValue: string | Date | Record<string, unknown> | unknown[] | undefined, | ||
| withSeconds: boolean | ||
| ): [string, string] { | ||
| if (Array.isArray(modelValue)) { | ||
| return [ | ||
| extractTimeFromPicker(String(modelValue[0] ?? ''), withSeconds), | ||
| extractTimeFromPicker(String(modelValue[1] ?? ''), withSeconds) | ||
| ] | ||
| } | ||
| if (typeof modelValue === 'string' && modelValue.includes(',')) { | ||
| const parts = modelValue.split(',').map((s) => s.trim()) | ||
| return [ | ||
| extractTimeFromPicker(parts[0] ?? '', withSeconds), | ||
| extractTimeFromPicker(parts[1] ?? '', withSeconds) | ||
| ] | ||
| } | ||
| return ['', ''] | ||
| } | ||
| const props = withDefaults( | ||
| defineProps<{ | ||
| modelValue?: string | Date | Record<string, unknown> | ||
| modelValue?: string | Date | Record<string, unknown> | unknown[] | ||
| config?: TimePickerConfig | ||
@@ -103,4 +153,4 @@ }>(), | ||
| const emit = defineEmits<{ | ||
| 'update:modelValue': [string] | ||
| change: [string] | ||
| 'update:modelValue': [string | string[]] | ||
| change: [string | string[]] | ||
| 'visible-change': [boolean] | ||
@@ -115,11 +165,7 @@ }>() | ||
| const borderEnabled = computed(() => { | ||
| if (cfg.value.border === false) return false | ||
| return true | ||
| }) | ||
| const borderEnabled = computed(() => cfg.value.border !== false) | ||
| const clearIconEnabled = computed(() => { | ||
| const clearableFlag: unknown = cfg.value.clearable ?? cfg.value['clearable'] | ||
| if (clearableFlag === false) return false | ||
| return true | ||
| return clearableFlag !== false | ||
| }) | ||
@@ -139,2 +185,12 @@ | ||
| const startPlaceholderText = computed(() => { | ||
| const explicit = startPlaceholderStr.value | ||
| return explicit.trim() !== '' ? explicit : '开始时间' | ||
| }) | ||
| const endPlaceholderText = computed(() => { | ||
| const explicit = endPlaceholderStr.value | ||
| return explicit.trim() !== '' ? explicit : '结束时间' | ||
| }) | ||
| const idAttr = computed(() => { | ||
@@ -160,3 +216,8 @@ const id = cfg.value.id | ||
| const rangeTimes = computed(() => | ||
| parseRangeTimes(props.modelValue, formatIncludesSeconds.value) | ||
| ) | ||
| const pickerSyncKey = computed(() => { | ||
| if (isRange.value) return `${rangeTimes.value[0]}|${rangeTimes.value[1]}` | ||
| const v = props.modelValue | ||
@@ -169,5 +230,4 @@ if (v == null || v === '') return 'empty' | ||
| const v = props.modelValue | ||
| if (v == null || v === '') { | ||
| return '' | ||
| } | ||
| if (v == null || v === '') return '' | ||
| if (Array.isArray(v)) return '' | ||
| if (typeof v === 'string') { | ||
@@ -188,2 +248,12 @@ if (hasDatePrefix(v)) return extractTimeFromPicker(v, formatIncludesSeconds.value) | ||
| const startPickerBind = computed(() => { | ||
| const t = rangeTimes.value[0] | ||
| return t ? toFullDateTimeForPicker(t) : '' | ||
| }) | ||
| const endPickerBind = computed(() => { | ||
| const t = rangeTimes.value[1] | ||
| return t ? toFullDateTimeForPicker(t) : '' | ||
| }) | ||
| const nameValueStr = computed(() => timeStr.value) | ||
@@ -196,2 +266,15 @@ | ||
| function emitRange(pair: [string, string]) { | ||
| const out: [string, string] = [pair[0] || '', pair[1] || ''] | ||
| emit('update:modelValue', out) | ||
| emit('change', out) | ||
| callConfigChange(out) | ||
| } | ||
| function onRangePartUpdate(index: 0 | 1, val: string) { | ||
| const next = [...rangeTimes.value] as [string, string] | ||
| next[index] = extractTimeFromPicker(val, formatIncludesSeconds.value) | ||
| emitRange(next) | ||
| } | ||
| function onUpdate(val: string) { | ||
@@ -218,7 +301,20 @@ const out = extractTimeFromPicker(val, formatIncludesSeconds.value) | ||
| .flm-time-unsupported { | ||
| font-size: 24rpx; | ||
| color: #ee0a24; | ||
| padding: 16rpx 0; | ||
| .flm-time-range { | ||
| display: flex; | ||
| flex-direction: row; | ||
| align-items: center; | ||
| gap: 12rpx; | ||
| width: 100%; | ||
| } | ||
| .flm-time-range > uni-datetime-picker { | ||
| flex: 1; | ||
| min-width: 0; | ||
| } | ||
| .flm-time-range-sep { | ||
| flex-shrink: 0; | ||
| font-size: 26rpx; | ||
| color: #969799; | ||
| } | ||
| </style> |
| <template> | ||
| <view class="flmDynamicForm"> | ||
| <view class="flmDynamicForm" :class="rootClass"> | ||
| <view v-for="(g, gi) in safeGroups" :key="groupKey(g, gi)" class="flm-group"> | ||
| <view v-if="g.title" class="flm-group-title-wrap"> | ||
| <view | ||
| v-if="collapseEnabled && groupTitle(g)" | ||
| class="flm-group-collapse-head" | ||
| @tap="toggleGroupOpen(gi)" | ||
| > | ||
| <view class="flm-group-title-bar" /> | ||
| <text class="flm-group-title">{{ String(g.title) }}</text> | ||
| <text class="flm-group-title flm-group-title--collapse">{{ groupTitle(g) }}</text> | ||
| <text class="flm-group-collapse-arrow">{{ groupOpen(gi) ? '▼' : '▶' }}</text> | ||
| </view> | ||
| <uni-forms | ||
| v-if="g.type === 'form'" | ||
| :model="ensureFormRow(g.id)" | ||
| :rules="rulesForGroup(g)" | ||
| label-position="left" | ||
| :label-width="formLabelWidth" | ||
| > | ||
| <uni-forms-item | ||
| v-for="item in formItemsWithProp(g)" | ||
| :key="String(item.prop)" | ||
| :label="item.label != null ? String(item.label) : ''" | ||
| :name="item.prop" | ||
| > | ||
| <flm-form-item-control | ||
| :item="item" | ||
| no-label | ||
| :model-value="fieldValue(g.id, item.prop)" | ||
| :is-edit="isEdit" | ||
| @update:model-value="(ev) => setField(g.id, item.prop, ev)" | ||
| /> | ||
| </uni-forms-item> | ||
| </uni-forms> | ||
| <flm-sub-form | ||
| v-else-if="g.type === 'subForm'" | ||
| :config="subFormConfigFor(g)" | ||
| :model-value="subFormGet(g.id)" | ||
| @update:model-value="(ev) => setSubForm(g.id, ev)" | ||
| /> | ||
| <view v-else-if="groupTitle(g)" class="flm-group-title-wrap"> | ||
| <view class="flm-group-title-bar" /> | ||
| <text class="flm-group-title">{{ groupTitle(g) }}</text> | ||
| </view> | ||
| <view v-show="!collapseEnabled || groupOpen(gi)" class="flm-group-body"> | ||
| <template v-if="g.type === 'form'"> | ||
| <view | ||
| v-for="(item, itemIndex) in decorativeGroupItems(g)" | ||
| :key="formItemKey(g, item, itemIndex)" | ||
| class="flm-form-item-wrap" | ||
| > | ||
| <view v-if="isSlotItem(item)" class="flm-slot-hint"> | ||
| <text class="flm-slot-hint-label">{{ slotItemLabel(item) }}</text> | ||
| <text class="flm-slot-hint-desc">自定义插槽需在页面通过同名插槽实现</text> | ||
| </view> | ||
| <flm-alert | ||
| v-else-if="isAlertItem(item)" | ||
| :config="alertConfigFor(item)" | ||
| /> | ||
| </view> | ||
| <uni-forms | ||
| v-if="formItemsWithProp(g).length" | ||
| :model="ensureFormRow(g.id)" | ||
| :rules="rulesForGroup(g)" | ||
| label-position="left" | ||
| :label-width="formLabelWidth" | ||
| > | ||
| <uni-forms-item | ||
| v-for="item in formItemsWithProp(g)" | ||
| :key="String(item.prop)" | ||
| :label="item.label != null ? String(item.label) : ''" | ||
| :name="item.prop" | ||
| > | ||
| <flm-form-item-control | ||
| :item="patchItemForMode(item)" | ||
| no-label | ||
| :model-value="fieldValue(g.id, item.prop)" | ||
| :mode="resolvedMode" | ||
| @update:model-value="(ev) => setField(g.id, item.prop, ev)" | ||
| /> | ||
| </uni-forms-item> | ||
| </uni-forms> | ||
| </template> | ||
| <flm-sub-form | ||
| v-else-if="g.type === 'subForm'" | ||
| :config="subFormConfigFor(g)" | ||
| :model-value="subFormGet(g.id)" | ||
| @update:model-value="(ev) => setSubForm(g.id, ev)" | ||
| /> | ||
| </view> | ||
| </view> | ||
| <view v-if="safeButtons.length && isEdit" class="flm-actions"> | ||
| <view v-if="safeButtons.length && showActionButtons" class="flm-actions"> | ||
| <flm-button | ||
@@ -49,3 +76,4 @@ v-for="(btn, bi) in safeButtons" | ||
| <script setup lang="ts"> | ||
| import { computed, reactive, watch } from 'vue' | ||
| import { computed, reactive, ref, watch } from 'vue' | ||
| import FlmAlert from '../../base/flmAlert/flmAlert.vue' | ||
| import FlmButton from '../../base/flmButton/flmButton.vue' | ||
@@ -67,6 +95,18 @@ import FlmFormItemControl from '../../base/flmFormItemControl/flmFormItemControl.vue' | ||
| type FormItem = DynamicFormItemConfig | ||
| type FormMode = 'edit' | 'read' | 'disabled' | ||
| function formItemsWithProp(g: FormGroup) { | ||
| const list = Array.isArray(g.items) ? g.items : [] | ||
| return list.filter((it) => typeof it.prop === 'string' && it.prop) as { | ||
| function groupItems(group: FormGroup) { | ||
| return Array.isArray(group.items) ? (group.items as FormItem[]) : [] | ||
| } | ||
| function itemHasFormProp(item: FormItem) { | ||
| return typeof item.prop === 'string' && String(item.prop).trim() !== '' | ||
| } | ||
| function decorativeGroupItems(group: FormGroup) { | ||
| return groupItems(group).filter((item) => !itemHasFormProp(item)) | ||
| } | ||
| function formItemsWithProp(group: FormGroup) { | ||
| return groupItems(group).filter((it) => typeof it.prop === 'string' && it.prop) as { | ||
| prop: string | ||
@@ -80,25 +120,54 @@ label?: unknown | ||
| function normalizeUniRules(r: unknown): Record<string, unknown>[] { | ||
| if (r == null) return [] | ||
| function isSlotItem(item: FormItem) { | ||
| return item.isSlot === true || item.isSlot === 'true' || item.isSlot === 1 | ||
| } | ||
| function isAlertItem(item: FormItem) { | ||
| const t = controlTypeOf(item) | ||
| return t === 'flmAlert' | ||
| } | ||
| function alertConfigFor(item: FormItem) { | ||
| const cc = item.controlConfig | ||
| return cc && typeof cc === 'object' ? (cc as Record<string, unknown>) : {} | ||
| } | ||
| function slotItemLabel(item: FormItem) { | ||
| if (item.label != null && String(item.label).trim() !== '') return String(item.label) | ||
| if (typeof item.prop === 'string' && item.prop) return `插槽:${item.prop}` | ||
| return '自定义插槽' | ||
| } | ||
| function formItemKey(group: FormGroup, item: FormItem, index: number) { | ||
| if (typeof item.prop === 'string' && item.prop) return `${String(group.id)}-${item.prop}` | ||
| return `${String(group.id)}-item-${index}` | ||
| } | ||
| function normalizeUniRules(ruleSource: unknown): Record<string, unknown>[] { | ||
| if (ruleSource == null) return [] | ||
| let arr: unknown[] = [] | ||
| if (Array.isArray(r)) arr = r | ||
| else if (typeof r === 'object' && r !== null && Array.isArray((r as { rules?: unknown[] }).rules)) { | ||
| arr = (r as { rules: unknown[] }).rules | ||
| if (Array.isArray(ruleSource)) arr = ruleSource | ||
| else if ( | ||
| typeof ruleSource === 'object' && | ||
| ruleSource !== null && | ||
| Array.isArray((ruleSource as { rules?: unknown[] }).rules) | ||
| ) { | ||
| arr = (ruleSource as { rules: unknown[] }).rules | ||
| } else return [] | ||
| const res: Record<string, unknown>[] = [] | ||
| for (const x of arr) { | ||
| if (!x || typeof x !== 'object') continue | ||
| const o = x as Record<string, unknown> | ||
| if (o.required === true || o.required === 'true') { | ||
| for (const entry of arr) { | ||
| if (!entry || typeof entry !== 'object') continue | ||
| const rule = entry as Record<string, unknown> | ||
| if (rule.required === true || rule.required === 'true') { | ||
| res.push({ | ||
| required: true, | ||
| errorMessage: String(o.message ?? o.errorMessage ?? '必填项') | ||
| errorMessage: String(rule.message ?? rule.errorMessage ?? '必填项') | ||
| }) | ||
| continue | ||
| } | ||
| const pat = o.pattern ?? o.regexp | ||
| if (typeof pat === 'string' || pat instanceof RegExp) { | ||
| const pattern = rule.pattern ?? rule.regexp | ||
| if (typeof pattern === 'string' || pattern instanceof RegExp) { | ||
| res.push({ | ||
| pattern: pat instanceof RegExp ? pat.source : pat, | ||
| errorMessage: String(o.message ?? o.errorMessage ?? '格式不正确') | ||
| pattern: pattern instanceof RegExp ? pattern.source : pattern, | ||
| errorMessage: String(rule.message ?? rule.errorMessage ?? '格式不正确') | ||
| }) | ||
@@ -110,7 +179,7 @@ } | ||
| function rulesForGroup(g: FormGroup) { | ||
| function rulesForGroup(group: FormGroup) { | ||
| const out: Record<string, { rules: Record<string, unknown>[] }> = {} | ||
| for (const it of formItemsWithProp(g)) { | ||
| const list = normalizeUniRules(it.rules) | ||
| if (list.length) out[it.prop] = { rules: list } | ||
| for (const item of formItemsWithProp(group)) { | ||
| const list = normalizeUniRules(item.rules) | ||
| if (list.length) out[item.prop] = { rules: list } | ||
| } | ||
@@ -121,34 +190,69 @@ return out | ||
| function controlTypeOf(item: { controlType?: unknown }) { | ||
| const t = item.controlType | ||
| if (t == null || t === '') return 'flmInput' | ||
| const s = String(t) | ||
| if (s === 'FlmEditor') return 'flmEditor' | ||
| return s | ||
| const controlType = item.controlType | ||
| if (controlType == null || controlType === '') return 'flmInput' | ||
| const typeString = String(controlType) | ||
| if (typeString === 'FlmEditor') return 'flmEditor' | ||
| return typeString | ||
| } | ||
| function defaultForItem(item: { controlType?: unknown; controlConfig?: Record<string, unknown> }) { | ||
| const t = controlTypeOf(item) | ||
| const cc = item.controlConfig || {} | ||
| if (t === 'flmSwitch') { | ||
| if (cc.inactiveValue !== undefined) return cc.inactiveValue | ||
| if (cc['inactive-value'] !== undefined) return cc['inactive-value'] | ||
| const controlType = controlTypeOf(item) | ||
| const controlConfig = item.controlConfig || {} | ||
| if (controlType === 'flmDatePicker') { | ||
| const pickerType = String(controlConfig.type ?? controlConfig['type'] ?? 'date') | ||
| if (['daterange', 'datetimerange', 'monthrange'].includes(pickerType)) return [] | ||
| return '' | ||
| } | ||
| if (controlType === 'flmTimePicker' && (controlConfig['is-range'] === true || controlConfig.isRange === true)) { | ||
| return ['', ''] | ||
| } | ||
| if (controlType === 'flmSelect' && controlConfig.multiple === true) return [] | ||
| if (controlType === 'flmSlider' && controlConfig.range === true) { | ||
| return [Number(controlConfig.min ?? 0), Number(controlConfig.max ?? 100)] | ||
| } | ||
| if (controlType === 'flmSwitch') { | ||
| if (controlConfig.inactiveValue !== undefined) return controlConfig.inactiveValue | ||
| if (controlConfig['inactive-value'] !== undefined) return controlConfig['inactive-value'] | ||
| return false | ||
| } | ||
| if (t === 'flmTime') return '' | ||
| if (t === 'flmCheckbox') { | ||
| if (controlType === 'flmTime') return '' | ||
| if (controlType === 'flmCheckbox') { | ||
| if (isFlmCheckboxSingleModeForFormItem(item)) return false | ||
| return [] | ||
| } | ||
| if (t === 'flmCheckboxGroup') return [] | ||
| if (t === 'flmInputNumber') return cc.min !== undefined ? Number(cc.min) : 0 | ||
| if (t === 'flmSlider') return cc.min !== undefined ? Number(cc.min) : 0 | ||
| if (t === 'flmRate') return 0 | ||
| if (t === 'flmCascader') return (cc.props as { emitPath?: boolean } | undefined)?.emitPath ? [] : '' | ||
| if (t === 'flmColorPicker') return '' | ||
| if (t === 'flmUpload') return [] | ||
| if (t === 'flmImage') return '' | ||
| if (t === 'flmSearchSelect') return '' | ||
| if (t === 'flmTimeSelect') return '' | ||
| if (t === 'flmTransfer') return [] | ||
| if (t === 'flmEditor') return '' | ||
| if (controlType === 'flmCheckboxGroup') return [] | ||
| if (controlType === 'flmInputNumber') return controlConfig.min !== undefined ? Number(controlConfig.min) : 0 | ||
| if (controlType === 'flmSlider') return controlConfig.min !== undefined ? Number(controlConfig.min) : 0 | ||
| if (controlType === 'flmRate') return 0 | ||
| if (controlType === 'flmCascader') { | ||
| const propsCfg = controlConfig.props as { emitPath?: boolean; multiple?: boolean } | undefined | ||
| if (propsCfg?.multiple || controlConfig.multiple === true) return [] | ||
| return propsCfg?.emitPath ? [] : '' | ||
| } | ||
| if (controlType === 'flmColorPicker') return '' | ||
| if (controlType === 'flmUpload') return [] | ||
| if (controlType === 'flmImage') return '' | ||
| if (controlType === 'flmSearchSelect') return '' | ||
| if (controlType === 'flmTimeSelect') return '' | ||
| if (controlType === 'flmTransfer') return [] | ||
| if (controlType === 'flmEditor') return '' | ||
| if (controlType === 'FlmCodeRuleInput') return '' | ||
| if (controlType === 'FlmBizUpload') { | ||
| return controlConfig.multiple === true ? [] : '' | ||
| } | ||
| if ( | ||
| controlType === 'FlmSysFlameUser' || | ||
| controlType === 'FlmSysFlameOrg' || | ||
| controlType === 'FlmSysFlameRole' || | ||
| controlType === 'FlmSysFlameMobileRole' || | ||
| controlType === 'FlmSysFlameBusinessStation' || | ||
| controlType === 'FlmSysFlameStation' || | ||
| controlType === 'FlmSysFlamePerson' || | ||
| controlType === 'FlmGenericDataSelect' | ||
| ) { | ||
| const defaultMultiple = | ||
| controlType === 'FlmSysFlameUser' && controlConfig.multiple === undefined | ||
| const isMultiple = controlConfig.multiple === true || defaultMultiple | ||
| return isMultiple ? [] : null | ||
| } | ||
| return '' | ||
@@ -162,2 +266,3 @@ } | ||
| isEdit?: boolean | ||
| mode?: FormMode | ||
| }>(), | ||
@@ -167,3 +272,4 @@ { | ||
| value: undefined, | ||
| isEdit: true | ||
| isEdit: true, | ||
| mode: 'edit' | ||
| } | ||
@@ -180,4 +286,15 @@ ) | ||
| const isEdit = computed(() => props.isEdit !== false) | ||
| const resolvedMode = computed((): FormMode => { | ||
| if (props.isEdit === false) return 'read' | ||
| const mode = props.mode | ||
| if (mode === 'read' || mode === 'disabled') return mode | ||
| return 'edit' | ||
| }) | ||
| const showActionButtons = computed(() => resolvedMode.value === 'edit') | ||
| const rootClass = computed(() => | ||
| resolvedMode.value !== 'edit' ? `flmDynamicForm--${resolvedMode.value}` : '' | ||
| ) | ||
| const formLabelWidth = computed(() => { | ||
@@ -194,2 +311,3 @@ const root = props.config && typeof props.config === 'object' ? (props.config as Record<string, unknown>) : null | ||
| ) | ||
| const safeButtons = computed<DynamicFormButtonConfig[]>(() => | ||
@@ -199,7 +317,38 @@ Array.isArray(props.config?.buttons) ? props.config!.buttons! : [] | ||
| const collapseEnabled = computed(() => { | ||
| const root = props.config as Record<string, unknown> | undefined | ||
| if (root?.collapseConfig === false) return false | ||
| return safeGroups.value.length > 1 | ||
| }) | ||
| const groupOpenState = ref<boolean[]>([]) | ||
| function groupTitle(group: FormGroup) { | ||
| return group.title != null && String(group.title).trim() !== '' ? String(group.title) : '' | ||
| } | ||
| function syncGroupOpenState() { | ||
| const count = safeGroups.value.length | ||
| const prev = groupOpenState.value | ||
| if (prev.length === count) return | ||
| groupOpenState.value = Array.from({ length: count }, (_, index) => prev[index] !== false) | ||
| } | ||
| watch(safeGroups, syncGroupOpenState, { immediate: true }) | ||
| function groupOpen(index: number) { | ||
| return groupOpenState.value[index] !== false | ||
| } | ||
| function toggleGroupOpen(index: number) { | ||
| const next = [...groupOpenState.value] | ||
| next[index] = !groupOpen(index) | ||
| groupOpenState.value = next | ||
| } | ||
| const formState = reactive<Record<string, Record<string, unknown>>>({}) | ||
| const subFormState = reactive<Record<string, unknown[]>>({}) | ||
| function groupKey(g: { id?: unknown }, index: number) { | ||
| return g.id != null ? String(g.id) : `g-${index}` | ||
| function groupKey(group: { id?: unknown }, index: number) { | ||
| return group.id != null ? String(group.id) : `g-${index}` | ||
| } | ||
@@ -214,7 +363,22 @@ | ||
| function subFormGet(groupId: unknown) { | ||
| const k = String(groupId) | ||
| if (!Array.isArray(subFormState[k])) subFormState[k] = [] | ||
| return subFormState[k] | ||
| const key = String(groupId) | ||
| if (!Array.isArray(subFormState[key])) subFormState[key] = [] | ||
| return subFormState[key] | ||
| } | ||
| function patchItemForMode(item: FormItem): FormItem { | ||
| if (resolvedMode.value === 'edit') return item | ||
| const controlConfig = | ||
| item.controlConfig && typeof item.controlConfig === 'object' | ||
| ? { ...(item.controlConfig as Record<string, unknown>) } | ||
| : {} | ||
| if (resolvedMode.value === 'disabled') { | ||
| controlConfig.disabled = true | ||
| } | ||
| if (resolvedMode.value === 'read') { | ||
| controlConfig.disabled = true | ||
| } | ||
| return { ...item, controlConfig } | ||
| } | ||
| function subFormConfigFor(group: FormGroup): SubFormConfig { | ||
@@ -232,6 +396,9 @@ const root = | ||
| : {} | ||
| const groupEditable = group.isEdit !== false && resolvedMode.value === 'edit' | ||
| return { | ||
| ...group, | ||
| ...inheritLabel, | ||
| isEdit: props.isEdit !== false && group.isEdit !== false | ||
| isEdit: groupEditable, | ||
| readAsText: resolvedMode.value === 'read', | ||
| disabled: resolvedMode.value === 'disabled' ? true : groupRecord.disabled === true | ||
| } | ||
@@ -241,5 +408,5 @@ } | ||
| function findItem(groupId: unknown, prop: string | undefined) { | ||
| const g = safeGroups.value.find((x) => String(x.id) === String(groupId)) | ||
| if (!g || !Array.isArray(g.items)) return null | ||
| return g.items.find((it) => it.prop === prop) || null | ||
| const group = safeGroups.value.find((entry) => String(entry.id) === String(groupId)) | ||
| if (!group || !Array.isArray(group.items)) return null | ||
| return group.items.find((entry) => entry.prop === prop) || null | ||
| } | ||
@@ -250,39 +417,63 @@ | ||
| const row = ensureFormRow(groupId) | ||
| const v = row[prop] | ||
| const value = row[prop] | ||
| const item = findItem(groupId, prop) | ||
| const t = item ? controlTypeOf(item) : 'flmInput' | ||
| if (t === 'flmCheckbox') { | ||
| const controlType = item ? controlTypeOf(item) : 'flmInput' | ||
| if (controlType === 'flmCheckbox') { | ||
| if (item && isFlmCheckboxSingleModeForFormItem(item)) { | ||
| if (v === undefined || v === null) return defaultForItem(item) | ||
| if (Array.isArray(v)) { | ||
| if (v.length === 0) return defaultForItem(item) | ||
| if (v.length === 1) return v[0] | ||
| if (value === undefined || value === null) return defaultForItem(item) | ||
| if (Array.isArray(value)) { | ||
| if (value.length === 0) return defaultForItem(item) | ||
| if (value.length === 1) return value[0] | ||
| return defaultForItem(item) | ||
| } | ||
| return v | ||
| return value | ||
| } | ||
| if (v === undefined || v === null) return item ? defaultForItem(item) : [] | ||
| return Array.isArray(v) ? v : [] | ||
| if (value === undefined || value === null) return item ? defaultForItem(item) : [] | ||
| return Array.isArray(value) ? value : [] | ||
| } | ||
| if (t === 'flmCheckboxGroup' || t === 'flmUpload' || t === 'flmTransfer') { | ||
| if (v === undefined || v === null) return item ? defaultForItem(item) : [] | ||
| return Array.isArray(v) ? v : [] | ||
| if (controlType === 'FlmBizUpload') { | ||
| if (value === undefined || value === null) return item ? defaultForItem(item) : '' | ||
| const uploadMultiple = item?.controlConfig?.multiple === true | ||
| if (uploadMultiple) return Array.isArray(value) ? value : [] | ||
| return value | ||
| } | ||
| if (t === 'flmInputNumber' || t === 'flmSlider' || t === 'flmRate') { | ||
| if (v === undefined || v === null) return item ? defaultForItem(item) : 0 | ||
| const n = Number(v) | ||
| return Number.isNaN(n) ? (item ? defaultForItem(item) : 0) : n | ||
| if ( | ||
| controlType === 'FlmSysFlameUser' || | ||
| controlType === 'FlmSysFlameOrg' || | ||
| controlType === 'FlmSysFlameRole' || | ||
| controlType === 'FlmSysFlameMobileRole' || | ||
| controlType === 'FlmSysFlameBusinessStation' || | ||
| controlType === 'FlmSysFlameStation' || | ||
| controlType === 'FlmSysFlamePerson' || | ||
| controlType === 'FlmGenericDataSelect' | ||
| ) { | ||
| if (value === undefined || value === null) return item ? defaultForItem(item) : null | ||
| const controlConfig = (item?.controlConfig || {}) as Record<string, unknown> | ||
| const defaultMultiple = | ||
| controlType === 'FlmSysFlameUser' && controlConfig.multiple === undefined | ||
| const isMultiple = controlConfig.multiple === true || defaultMultiple | ||
| if (isMultiple) return Array.isArray(value) ? value : value == null || value === '' ? [] : [value] | ||
| return value | ||
| } | ||
| if (t === 'flmTime') { | ||
| if (v === undefined || v === null) return item ? defaultForItem(item) : '' | ||
| return String(v) | ||
| if (controlType === 'flmCheckboxGroup' || controlType === 'flmUpload' || controlType === 'flmTransfer') { | ||
| if (value === undefined || value === null) return item ? defaultForItem(item) : [] | ||
| return Array.isArray(value) ? value : [] | ||
| } | ||
| if (v === undefined || v === null) { | ||
| if (controlType === 'flmInputNumber' || controlType === 'flmSlider' || controlType === 'flmRate') { | ||
| if (value === undefined || value === null) return item ? defaultForItem(item) : 0 | ||
| const numberValue = Number(value) | ||
| return Number.isNaN(numberValue) ? (item ? defaultForItem(item) : 0) : numberValue | ||
| } | ||
| if (controlType === 'flmTime') { | ||
| if (value === undefined || value === null) return item ? defaultForItem(item) : '' | ||
| return String(value) | ||
| } | ||
| if (value === undefined || value === null) { | ||
| return item ? defaultForItem(item) : '' | ||
| } | ||
| return v | ||
| return value | ||
| } | ||
| function setField(groupId: unknown, prop: string | undefined, val: unknown) { | ||
| if (!prop) return | ||
| if (!prop || resolvedMode.value !== 'edit') return | ||
| ensureFormRow(groupId)[prop] = val | ||
@@ -293,4 +484,5 @@ emit('update:value', buildPayload()) | ||
| function setSubForm(groupId: unknown, rows: unknown) { | ||
| if (resolvedMode.value !== 'edit') return | ||
| subFormState[String(groupId)] = Array.isArray(rows) | ||
| ? rows.map((r) => (r && typeof r === 'object' ? { ...(r as Record<string, unknown>) } : {})) | ||
| ? rows.map((row) => (row && typeof row === 'object' ? { ...(row as Record<string, unknown>) } : {})) | ||
| : [] | ||
@@ -302,48 +494,52 @@ emit('update:value', buildPayload()) | ||
| const source = props.value && typeof props.value === 'object' ? { ...props.value } : {} | ||
| safeGroups.value.forEach((g) => { | ||
| if (g.type === 'form' && g.id != null) { | ||
| const id = String(g.id) | ||
| safeGroups.value.forEach((group) => { | ||
| if (group.type === 'form' && group.id != null) { | ||
| const id = String(group.id) | ||
| const row = ensureFormRow(id) | ||
| ;(g.items || []).forEach((it) => { | ||
| const p = it.prop | ||
| if (typeof p !== 'string' || !p) return | ||
| if (Object.prototype.hasOwnProperty.call(source, p)) { | ||
| const rawVal = (source as Record<string, unknown>)[p] | ||
| const ct = controlTypeOf(it) | ||
| if (ct === 'flmCheckbox') { | ||
| if (isFlmCheckboxSingleModeForFormItem(it)) { | ||
| ;(group.items || []).forEach((item) => { | ||
| const prop = item.prop | ||
| if (typeof prop !== 'string' || !prop) return | ||
| if (Object.prototype.hasOwnProperty.call(source, prop)) { | ||
| const rawVal = (source as Record<string, unknown>)[prop] | ||
| const controlType = controlTypeOf(item) | ||
| if (controlType === 'flmCheckbox') { | ||
| if (isFlmCheckboxSingleModeForFormItem(item)) { | ||
| if (rawVal === undefined || rawVal === null) { | ||
| row[p] = defaultForItem(it) | ||
| row[prop] = defaultForItem(item) | ||
| } else if (Array.isArray(rawVal)) { | ||
| if (rawVal.length === 0) row[p] = defaultForItem(it) | ||
| else if (rawVal.length === 1) row[p] = rawVal[0] | ||
| else row[p] = defaultForItem(it) | ||
| if (rawVal.length === 0) row[prop] = defaultForItem(item) | ||
| else if (rawVal.length === 1) row[prop] = rawVal[0] | ||
| else row[prop] = defaultForItem(item) | ||
| } else { | ||
| row[p] = rawVal | ||
| row[prop] = rawVal | ||
| } | ||
| } else { | ||
| row[p] = Array.isArray(rawVal) ? rawVal : rawVal == null ? [] : [rawVal] | ||
| row[prop] = Array.isArray(rawVal) ? rawVal : rawVal == null ? [] : [rawVal] | ||
| } | ||
| } else if (ct === 'flmCheckboxGroup') { | ||
| row[p] = Array.isArray(rawVal) ? rawVal : rawVal == null ? [] : [rawVal] | ||
| } else if (ct === 'flmUpload' || ct === 'flmTransfer') { | ||
| row[p] = Array.isArray(rawVal) ? rawVal : [] | ||
| } else if (controlType === 'flmCheckboxGroup') { | ||
| row[prop] = Array.isArray(rawVal) ? rawVal : rawVal == null ? [] : [rawVal] | ||
| } else if (controlType === 'flmUpload' || controlType === 'flmTransfer') { | ||
| row[prop] = Array.isArray(rawVal) ? rawVal : [] | ||
| } else { | ||
| row[p] = rawVal | ||
| row[prop] = rawVal | ||
| } | ||
| return | ||
| } | ||
| const cc = it.controlConfig | ||
| if (cc && typeof cc === 'object' && Object.prototype.hasOwnProperty.call(cc, 'modelValue')) { | ||
| row[p] = cc.modelValue | ||
| const controlConfig = item.controlConfig | ||
| if ( | ||
| controlConfig && | ||
| typeof controlConfig === 'object' && | ||
| Object.prototype.hasOwnProperty.call(controlConfig, 'modelValue') | ||
| ) { | ||
| row[prop] = controlConfig.modelValue | ||
| return | ||
| } | ||
| row[p] = defaultForItem(it) | ||
| row[prop] = defaultForItem(item) | ||
| }) | ||
| } else if (g.type === 'subForm' && g.id != null) { | ||
| const id = String(g.id) | ||
| } else if (group.type === 'subForm' && group.id != null) { | ||
| const id = String(group.id) | ||
| const rawRows = (source as Record<string, unknown>)[id] | ||
| subFormState[id] = Array.isArray(rawRows) | ||
| ? rawRows.map((r) => | ||
| r && typeof r === 'object' ? { ...(r as Record<string, unknown>) } : {} | ||
| ? rawRows.map((row) => | ||
| row && typeof row === 'object' ? { ...(row as Record<string, unknown>) } : {} | ||
| ) | ||
@@ -365,10 +561,10 @@ : [] | ||
| const out = props.value && typeof props.value === 'object' ? { ...props.value } : {} | ||
| safeGroups.value.forEach((g) => { | ||
| if (g.type === 'form' && g.id != null) { | ||
| Object.assign(out, { ...formState[String(g.id)] }) | ||
| } else if (g.type === 'subForm' && g.id != null) { | ||
| const k = String(g.id) | ||
| const arr = subFormState[k] | ||
| ;(out as Record<string, unknown>)[k] = Array.isArray(arr) | ||
| ? arr.map((r) => ({ ...(r as Record<string, unknown>) })) | ||
| safeGroups.value.forEach((group) => { | ||
| if (group.type === 'form' && group.id != null) { | ||
| Object.assign(out, { ...formState[String(group.id)] }) | ||
| } else if (group.type === 'subForm' && group.id != null) { | ||
| const key = String(group.id) | ||
| const rows = subFormState[key] | ||
| ;(out as Record<string, unknown>)[key] = Array.isArray(rows) | ||
| ? rows.map((row) => ({ ...(row as Record<string, unknown>) })) | ||
| : [] | ||
@@ -415,2 +611,12 @@ } | ||
| } | ||
| .flm-group-body { | ||
| width: 100%; | ||
| } | ||
| .flm-group-collapse-head { | ||
| display: flex; | ||
| flex-direction: row; | ||
| align-items: center; | ||
| margin-bottom: 20rpx; | ||
| gap: 14rpx; | ||
| } | ||
| .flm-group-title-wrap { | ||
@@ -437,2 +643,28 @@ display: flex; | ||
| } | ||
| .flm-group-collapse-arrow { | ||
| font-size: 24rpx; | ||
| color: #969799; | ||
| flex-shrink: 0; | ||
| } | ||
| .flm-form-item-wrap { | ||
| margin-bottom: 8rpx; | ||
| } | ||
| .flm-slot-hint { | ||
| padding: 20rpx; | ||
| margin-bottom: 16rpx; | ||
| background-color: #f7f8fa; | ||
| border-radius: 8rpx; | ||
| border: 1px dashed #dcdee0; | ||
| } | ||
| .flm-slot-hint-label { | ||
| display: block; | ||
| font-size: 28rpx; | ||
| color: #323233; | ||
| margin-bottom: 8rpx; | ||
| } | ||
| .flm-slot-hint-desc { | ||
| display: block; | ||
| font-size: 24rpx; | ||
| color: #969799; | ||
| } | ||
| .flm-actions { | ||
@@ -444,2 +676,6 @@ display: flex; | ||
| } | ||
| .flmDynamicForm--read, | ||
| .flmDynamicForm--disabled { | ||
| opacity: 0.98; | ||
| } | ||
| </style> |
@@ -16,2 +16,16 @@ <template> | ||
| </view> | ||
| <view | ||
| v-for="(decorativeItem, decorativeIndex) in decorativeItems" | ||
| :key="`dec-${ri}-${decorativeIndex}`" | ||
| class="flm-sub-decorative" | ||
| > | ||
| <view v-if="isSlotItem(decorativeItem)" class="flm-slot-hint"> | ||
| <text class="flm-slot-hint-label">{{ slotItemLabel(decorativeItem) }}</text> | ||
| <text class="flm-slot-hint-desc">自定义插槽需在页面通过同名插槽实现</text> | ||
| </view> | ||
| <flm-alert | ||
| v-else-if="isAlertItem(decorativeItem)" | ||
| :config="alertConfigFor(decorativeItem)" | ||
| /> | ||
| </view> | ||
| <uni-forms :model="row" label-position="left" :label-width="subFormLabelWidth"> | ||
@@ -24,8 +38,11 @@ <uni-forms-item | ||
| > | ||
| <text v-if="readAsText && typeof it.prop === 'string'" class="flm-sub-read-text"> | ||
| {{ formatReadCell(row[it.prop]) }} | ||
| </text> | ||
| <flm-form-item-control | ||
| v-if="typeof it.prop === 'string'" | ||
| v-else-if="typeof it.prop === 'string'" | ||
| :item="it" | ||
| no-label | ||
| :model-value="row[it.prop]" | ||
| :is-edit="effectiveEdit" | ||
| :mode="subFormMode" | ||
| @update:model-value="(ev) => patchRowFromField(ri, it, ev)" | ||
@@ -44,2 +61,3 @@ /> | ||
| import { computed, ref, watch } from 'vue' | ||
| import FlmAlert from '../../base/flmAlert/flmAlert.vue' | ||
| import FlmFormItemControl from '../../base/flmFormItemControl/flmFormItemControl.vue' | ||
@@ -107,2 +125,40 @@ import { isFlmCheckboxSingleModeForFormItem } from '../../../common/checkboxMode' | ||
| const readAsText = computed(() => props.config.readAsText === true) | ||
| const subFormMode = computed((): 'edit' | 'read' | 'disabled' => { | ||
| if (readAsText.value) return 'read' | ||
| if (props.config.disabled === true) return 'disabled' | ||
| if (!effectiveEdit.value) return 'read' | ||
| return 'edit' | ||
| }) | ||
| function isSlotItem(item: FormItem) { | ||
| return item.isSlot === true || item.isSlot === 'true' || item.isSlot === 1 | ||
| } | ||
| function isAlertItem(item: FormItem) { | ||
| return controlTypeOf(item) === 'flmAlert' | ||
| } | ||
| function alertConfigFor(item: FormItem) { | ||
| const controlConfig = item.controlConfig | ||
| return controlConfig && typeof controlConfig === 'object' | ||
| ? (controlConfig as Record<string, unknown>) | ||
| : {} | ||
| } | ||
| function slotItemLabel(item: FormItem) { | ||
| if (item.label != null && String(item.label).trim() !== '') return String(item.label) | ||
| if (typeof item.prop === 'string' && item.prop) return `插槽:${item.prop}` | ||
| return '自定义插槽' | ||
| } | ||
| function formatReadCell(value: unknown) { | ||
| if (value === undefined || value === null || value === '') return '—' | ||
| if (typeof value === 'boolean') return value ? '是' : '否' | ||
| if (Array.isArray(value)) return value.map((entry) => String(entry)).join('、') | ||
| if (typeof value === 'object') return JSON.stringify(value) | ||
| return String(value) | ||
| } | ||
| const subFormLabelWidth = computed(() => { | ||
@@ -115,7 +171,15 @@ const configRecord = props.config as Record<string, unknown> | ||
| const formItems = computed(() => { | ||
| const allItems = computed(() => { | ||
| const raw = Array.isArray(props.config.items) ? (props.config.items as FormItem[]) : [] | ||
| return raw.filter((it) => typeof it.prop === 'string' && it.controlType) | ||
| return raw.filter((it) => it.controlType) | ||
| }) | ||
| const formItems = computed(() => | ||
| allItems.value.filter((it) => typeof it.prop === 'string' && String(it.prop)) | ||
| ) | ||
| const decorativeItems = computed(() => | ||
| allItems.value.filter((it) => !(typeof it.prop === 'string' && String(it.prop))) | ||
| ) | ||
| const rowExpandedFlags = ref<boolean[]>([]) | ||
@@ -258,3 +322,3 @@ | ||
| function patchRowFromField(rowIndex: number, item: FormItem, val: unknown) { | ||
| if (typeof item.prop !== 'string') return | ||
| if (typeof item.prop !== 'string' || subFormMode.value !== 'edit') return | ||
| patchRow(rowIndex, item.prop, val) | ||
@@ -343,2 +407,27 @@ } | ||
| } | ||
| .flm-sub-decorative { | ||
| margin-bottom: 16rpx; | ||
| } | ||
| .flm-sub-read-text { | ||
| font-size: 28rpx; | ||
| color: #323233; | ||
| line-height: 1.5; | ||
| } | ||
| .flm-slot-hint { | ||
| padding: 16rpx; | ||
| background-color: #f7f8fa; | ||
| border-radius: 8rpx; | ||
| border: 1px dashed #dcdee0; | ||
| } | ||
| .flm-slot-hint-label { | ||
| display: block; | ||
| font-size: 26rpx; | ||
| color: #323233; | ||
| margin-bottom: 6rpx; | ||
| } | ||
| .flm-slot-hint-desc { | ||
| display: block; | ||
| font-size: 24rpx; | ||
| color: #969799; | ||
| } | ||
| </style> |
180550
23.39%35
2.94%