🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

flame-uni

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

flame-uni - npm Package Compare versions

Comparing version
0.0.2
to
0.0.3
+210
uni_modules/flame-uni/components/base/flmAlert/flmAlert.vue
<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>