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

flame-uni

Package Overview
Dependencies
Maintainers
1
Versions
4
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.4
to
0.0.5
+21
uni_modules/flame-uni/common/datePickerValueFormat.ts
export const DATE_PICKER_VALUE_FORMAT_DATE = 'YYYY-MM-DD'
export const DATE_PICKER_VALUE_FORMAT_DATETIME = 'YYYY-MM-DD HH:mm:ss'
export function isDatePickerTypeWithTime(type: unknown): boolean {
if (type == null) return false
const normalizedType = String(type).trim().toLowerCase()
if (!normalizedType) return false
return normalizedType.includes('time')
}
export function resolveDatePickerValueFormat(
type: unknown,
explicitValueFormat?: unknown
): string {
if (explicitValueFormat != null && String(explicitValueFormat).trim() !== '') {
return String(explicitValueFormat)
}
return isDatePickerTypeWithTime(type)
? DATE_PICKER_VALUE_FORMAT_DATETIME
: DATE_PICKER_VALUE_FORMAT_DATE
}
$flm-field-disabled-bg: #f7f8fa;
$flm-field-disabled-text: #646566;
@mixin flm-field-disabled-deep {
:deep(.uni-easyinput__content.is-disabled) {
background-color: $flm-field-disabled-bg !important;
opacity: 1 !important;
}
:deep(.uni-easyinput__content.is-disabled .uni-easyinput__content-input),
:deep(.uni-easyinput__content.is-disabled .uni-easyinput__content-textarea) {
color: $flm-field-disabled-text !important;
-webkit-text-fill-color: $flm-field-disabled-text;
}
:deep(.uni-easyinput__content.is-disabled .uni-easyinput__placeholder-class) {
color: $flm-field-disabled-text !important;
}
:deep(.uni-date-editor--x__disabled) {
opacity: 1 !important;
cursor: default !important;
}
:deep(.uni-date-editor--x__disabled .uni-date-x) {
background-color: $flm-field-disabled-bg !important;
}
:deep(.uni-select--disabled) {
background-color: $flm-field-disabled-bg !important;
}
:deep(.uni-select--disabled .uni-select__input-text) {
color: $flm-field-disabled-text !important;
}
:deep(.uni-select--disabled .uni-select__input-placeholder) {
color: $flm-field-disabled-text !important;
}
:deep(.input-value-border),
:deep(.input-value.input-value-border) {
background-color: $flm-field-disabled-bg !important;
}
:deep(.text-color),
:deep(.placeholder) {
color: $flm-field-disabled-text !important;
}
}
import type { InjectionKey } from 'vue'
export type FlameRequestFn = (params: {
projectName?: string
tableName: string
flameMethod: string
data?: Record<string, unknown>
service?: string
suffix?: string
}) => Promise<unknown>
export const flameRequestContextKey: InjectionKey<FlameRequestFn> = Symbol('flameRequest')
import type { FlmTimeFormatPreset } from 'flame-uni-types'
function pad2(value: number) {
if (!Number.isFinite(value)) return '00'
return value < 10 ? `0${value}` : String(value)
}
export function resolveFlmTimeFormat(
formatPreset?: FlmTimeFormatPreset,
format?: string
): string {
const preset = (formatPreset ?? 'date') as FlmTimeFormatPreset
const custom = (format && String(format).trim()) || 'YYYY-MM-DD HH:mm:ss'
if (preset === 'date') return 'date:YYYY-MM-DD'
if (preset === 'time') return 'time:HH:mm:ss'
return `custom:${custom}`
}
function formatCustomDateTime(dateValue: Date, formatString: string) {
const trimmed = String(formatString).trim() || 'YYYY-MM-DD HH:mm:ss'
const segments = trimmed.split(/\s+/)
const datePart = segments[0] || 'YYYY-MM-DD'
const timePart = segments[1] || ''
const dateOutput = datePart
.replace(/YYYY/g, String(dateValue.getFullYear()))
.replace(/MM/g, pad2(dateValue.getMonth() + 1))
.replace(/DD/g, pad2(dateValue.getDate()))
if (!timePart) return dateOutput
const timeOutput = timePart
.replace(/HH/g, pad2(dateValue.getHours()))
.replace(/mm/g, pad2(dateValue.getMinutes()))
.replace(/ss/g, pad2(dateValue.getSeconds()))
return `${dateOutput} ${timeOutput}`
}
export function formatFlmTimeNow(
formatPreset?: FlmTimeFormatPreset,
format?: string,
at: Date = new Date()
): string {
const resolved = resolveFlmTimeFormat(formatPreset, format)
if (resolved.startsWith('date:')) {
return `${at.getFullYear()}-${pad2(at.getMonth() + 1)}-${pad2(at.getDate())}`
}
if (resolved.startsWith('time:')) {
return `${pad2(at.getHours())}:${pad2(at.getMinutes())}:${pad2(at.getSeconds())}`
}
const custom = resolved.slice('custom:'.length)
return formatCustomDateTime(at, custom)
}
import type { DynamicFormItemConfig } from 'flame-uni-types'
import { formatFlmTimeNow } from './flmTimeFormat'
type FormModel = Record<string, unknown>
function isSlotItem(formItem: DynamicFormItemConfig) {
return formItem.isSlot === true || formItem.isSlot === 'true' || formItem.isSlot === 1
}
export function updateSubFormRowFromModel(
row: FormModel,
model: FormModel,
items: DynamicFormItemConfig[] | undefined,
isSearch = false
) {
items?.forEach((formItem) => {
if (isSlotItem(formItem) || typeof formItem.prop !== 'string') return
let modelValue = model[formItem.prop]
if (!isSearch && formItem.prop.indexOf('.') !== -1) {
const tmpProps = formItem.prop.split('.')
const fkKey = tmpProps[0]
if (/_id$/.test(fkKey) && model[fkKey] && typeof model[fkKey] === 'object') {
modelValue = (model[fkKey] as Record<string, unknown>)[tmpProps[1]]
}
}
row[formItem.prop] = modelValue
})
}
export function prefillSubFormRowFlmTimeFields(
row: FormModel,
items: DynamicFormItemConfig[] | undefined
) {
items?.forEach((formItem) => {
if (isSlotItem(formItem) || typeof formItem.prop !== 'string') return
const controlType = formItem.controlType != null ? String(formItem.controlType) : ''
if (controlType !== 'flmTime') return
const controlConfig = (formItem.controlConfig || {}) as Record<string, unknown>
if (controlConfig.staticDisplay === true) return
row[formItem.prop] = formatFlmTimeNow(
controlConfig.formatPreset as 'date' | 'time' | 'custom' | undefined,
controlConfig.format as string | undefined
)
})
}
import type { FlmCalcConfig, FlmCalcMode, FlmCalcSubmitValue, FlmCalcVariable } from 'flame-uni-types'
export type { FlmCalcVariable }
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
type TokenType = 'number' | 'identifier' | 'operator' | 'lparen' | 'rparen'
interface Token {
type: TokenType
value: string
}
function tokenize(expression: string): Token[] {
const tokens: Token[] = []
let index = 0
const source = expression.trim()
while (index < source.length) {
const char = source[index]
if (/\s/.test(char)) {
index += 1
continue
}
if (char === '(') {
tokens.push({ type: 'lparen', value: char })
index += 1
continue
}
if (char === ')') {
tokens.push({ type: 'rparen', value: char })
index += 1
continue
}
if ('+-*/%'.includes(char)) {
tokens.push({ type: 'operator', value: char })
index += 1
continue
}
if (/[0-9.]/.test(char)) {
let numberText = char
index += 1
while (index < source.length && /[0-9.]/.test(source[index])) {
numberText += source[index]
index += 1
}
tokens.push({ type: 'number', value: numberText })
continue
}
if (/[A-Za-z_]/.test(char)) {
let identifier = char
index += 1
while (index < source.length && /[A-Za-z0-9_]/.test(source[index])) {
identifier += source[index]
index += 1
}
tokens.push({ type: 'identifier', value: identifier })
continue
}
throw new Error(`invalid character: ${char}`)
}
return tokens
}
class ExpressionParser {
private tokens: Token[]
private position = 0
private scope: Record<string, number>
constructor(tokens: Token[], scope: Record<string, number>) {
this.tokens = tokens
this.scope = scope
}
parse(): number {
const result = this.parseExpression()
if (this.peek()?.type !== undefined) {
throw new Error('unexpected token')
}
if (!Number.isFinite(result)) {
throw new Error('invalid result')
}
return result
}
private peek(): Token | undefined {
return this.tokens[this.position]
}
private consume(): Token {
const token = this.tokens[this.position]
this.position += 1
return token
}
private parseExpression(): number {
let value = this.parseTerm()
while (this.peek()?.type === 'operator' && (this.peek()?.value === '+' || this.peek()?.value === '-')) {
const operator = this.consume().value
const right = this.parseTerm()
value = operator === '+' ? value + right : value - right
}
return value
}
private parseTerm(): number {
let value = this.parseFactor()
while (
this.peek()?.type === 'operator' &&
(this.peek()?.value === '*' || this.peek()?.value === '/' || this.peek()?.value === '%')
) {
const operator = this.consume().value
const right = this.parseFactor()
if (operator === '*') {
value = value * right
} else if (operator === '/') {
if (right === 0) throw new Error('division by zero')
value = value / right
} else {
if (right === 0) throw new Error('modulo by zero')
value = value % right
}
}
return value
}
private parseFactor(): number {
const token = this.peek()
if (!token) throw new Error('unexpected end')
if (token.type === 'operator' && token.value === '-') {
this.consume()
return -this.parseFactor()
}
if (token.type === 'operator' && token.value === '+') {
this.consume()
return this.parseFactor()
}
if (token.type === 'lparen') {
this.consume()
const inner = this.parseExpression()
if (this.consume()?.type !== 'rparen') throw new Error('missing closing parenthesis')
return inner
}
if (token.type === 'number') {
this.consume()
const parsed = Number(token.value)
if (!Number.isFinite(parsed)) throw new Error('invalid number')
return parsed
}
if (token.type === 'identifier') {
this.consume()
if (!IDENTIFIER_PATTERN.test(token.value)) throw new Error('invalid identifier')
if (!Object.prototype.hasOwnProperty.call(this.scope, token.value)) {
throw new Error(`unknown variable: ${token.value}`)
}
return this.scope[token.value]
}
throw new Error('invalid expression')
}
}
export function normalizeNumericOperand(value: unknown, emptyAsZero: boolean): number | null {
if (value === null || value === undefined || value === '') {
return emptyAsZero ? 0 : null
}
const numericValue = typeof value === 'number' ? value : Number(String(value).trim())
if (!Number.isFinite(numericValue)) {
return emptyAsZero ? 0 : null
}
return numericValue
}
export function buildCalcScope(
variables: FlmCalcVariable[],
getValue: (prop: string) => unknown,
emptyAsZero: boolean
): Record<string, number> | null {
const scope: Record<string, number> = {}
for (const variable of variables) {
const key = String(variable.key ?? '').trim()
const prop = String(variable.prop ?? '').trim()
if (!key || !prop || !IDENTIFIER_PATTERN.test(key)) {
return null
}
const numericValue = normalizeNumericOperand(getValue(prop), emptyAsZero)
if (numericValue === null) return null
scope[key] = numericValue
}
return scope
}
export function evaluateCalcExpression(
expression: string,
variables: FlmCalcVariable[],
getValue: (prop: string) => unknown,
options?: { emptyAsZero?: boolean }
): number | null {
const trimmedExpression = String(expression ?? '').trim()
if (!trimmedExpression || !Array.isArray(variables) || variables.length === 0) {
return null
}
const emptyAsZero = options?.emptyAsZero !== false
const scope = buildCalcScope(variables, getValue, emptyAsZero)
if (!scope) return null
try {
const tokens = tokenize(trimmedExpression)
const parser = new ExpressionParser(tokens, scope)
return parser.parse()
} catch {
return null
}
}
export function formatCalcResult(value: number | null, precision = 2): string {
if (value === null || !Number.isFinite(value)) return '--'
const safePrecision = Number.isFinite(precision) ? Math.max(0, Math.min(20, Math.trunc(precision))) : 2
return Number(value.toFixed(safePrecision)).toString()
}
export function sanitizeCalcVariableKey(rawKey: string, fallbackIndex: number): string {
const trimmed = String(rawKey ?? '').trim()
if (IDENTIFIER_PATTERN.test(trimmed)) return trimmed
const fromProp = trimmed.replace(/[^A-Za-z0-9_]/g, '_').replace(/^(\d)/, '_$1')
if (IDENTIFIER_PATTERN.test(fromProp)) return fromProp
return `field_${fallbackIndex + 1}`
}
export function ensureUniqueCalcVariableKey(
variable: FlmCalcVariable,
index: number,
variables: FlmCalcVariable[]
): FlmCalcVariable {
const seed = String(variable.controlId || variable.prop || variable.label || `field_${index + 1}`)
let key = sanitizeCalcVariableKey(seed, index)
if (!variables.slice(0, index).some((item) => item.key === key)) {
return { ...variable, key }
}
let suffix = index + 1
while (variables.slice(0, index).some((item) => item.key === key) || key === variable.key) {
key = sanitizeCalcVariableKey(`${seed}_${suffix}`, suffix)
suffix += 1
}
return { ...variable, key }
}
function extractOrderedExpressionIdentifiers(expression: string): string[] {
try {
return tokenize(String(expression ?? '').trim())
.filter((token) => token.type === 'identifier')
.map((token) => token.value)
} catch {
return []
}
}
function replaceNthWholeIdentifier(
expression: string,
identifier: string,
replacement: string,
occurrenceIndex: number
): string {
const pattern = new RegExp(`\\b${identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g')
let matchIndex = 0
return expression.replace(pattern, (matched) => {
if (matchIndex === occurrenceIndex) {
matchIndex += 1
return replacement
}
matchIndex += 1
return matched
})
}
export function repairCalcConfigForEvaluation(
config: Pick<FlmCalcConfig, 'expression' | 'variables'>
): Pick<FlmCalcConfig, 'expression' | 'variables'> {
const sourceVariables = Array.isArray(config.variables) ? config.variables : []
if (!sourceVariables.length) {
return { expression: String(config.expression ?? ''), variables: [] }
}
const variables = sourceVariables.map((variable, index, list) =>
ensureUniqueCalcVariableKey(variable, index, list)
)
let expression = String(config.expression ?? '').trim()
const orderedIdentifiers = extractOrderedExpressionIdentifiers(expression)
if (
orderedIdentifiers.length === variables.length &&
orderedIdentifiers.some((identifier, index) => identifier !== variables[index]?.key)
) {
let remappedExpression = expression
const replaceCounts = new Map<string, number>()
orderedIdentifiers.forEach((identifier, index) => {
const targetKey = String(variables[index]?.key ?? '').trim()
if (!identifier || !targetKey || identifier === targetKey) return
const occurrenceIndex = replaceCounts.get(identifier) ?? 0
remappedExpression = replaceNthWholeIdentifier(
remappedExpression,
identifier,
targetKey,
occurrenceIndex
)
replaceCounts.set(identifier, occurrenceIndex + 1)
})
expression = remappedExpression
}
return { expression, variables }
}
export function extractExpressionVariableKeys(expression: string): string[] {
const tokens = tokenize(String(expression ?? '').trim())
const keys: string[] = []
for (const token of tokens) {
if (token.type !== 'identifier') continue
if (!keys.includes(token.value)) keys.push(token.value)
}
return keys
}
export function resolveCalcMode(calcMode?: FlmCalcMode): FlmCalcMode {
return calcMode === 'max' || calcMode === 'min' ? calcMode : 'formula'
}
function wrapFieldTokenLabel(label: string): string {
return `【${String(label ?? '').trim()}】`
}
function expressionToDisplayText(expression: string, variables: FlmCalcVariable[]): string {
const trimmed = String(expression ?? '').trim()
if (!trimmed) return ''
let displayText = trimmed
const sortedVariables = [...variables].sort((left, right) => right.key.length - left.key.length)
for (const variable of sortedVariables) {
const key = String(variable.key ?? '').trim()
const label = String(variable.label ?? variable.key ?? '').trim()
if (!key) continue
const keyPattern = new RegExp(`\\b${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g')
displayText = displayText.replace(keyPattern, wrapFieldTokenLabel(label))
}
return displayText
}
export function evaluateMaxMinValue(
calcMode: 'max' | 'min',
variables: FlmCalcVariable[],
getValue: (prop: string) => unknown,
options?: { emptyAsZero?: boolean }
): number | null {
if (!Array.isArray(variables) || variables.length === 0) return null
const emptyAsZero = options?.emptyAsZero !== false
const numericValues: number[] = []
for (const variable of variables) {
const prop = String(variable.prop ?? '').trim()
if (!prop) continue
const numericValue = normalizeNumericOperand(getValue(prop), emptyAsZero)
if (numericValue === null) continue
numericValues.push(numericValue)
}
if (!numericValues.length) return null
return calcMode === 'max' ? Math.max(...numericValues) : Math.min(...numericValues)
}
export function evaluateCalcValue(
config: Pick<FlmCalcConfig, 'calcMode' | 'expression' | 'variables' | 'emptyAsZero'>,
getValue: (prop: string) => unknown
): number | null {
const calcMode = resolveCalcMode(config.calcMode)
const emptyAsZero = config.emptyAsZero !== false
if (calcMode === 'max' || calcMode === 'min') {
const variables = Array.isArray(config.variables) ? config.variables : []
return evaluateMaxMinValue(calcMode, variables, getValue, { emptyAsZero })
}
const repairedConfig = repairCalcConfigForEvaluation(config)
return evaluateCalcExpression(repairedConfig.expression || '', repairedConfig.variables, getValue, {
emptyAsZero
})
}
export function buildCalcFormulaDescription(
config: Pick<FlmCalcConfig, 'calcMode' | 'expression' | 'variables'>
): string {
const calcMode = resolveCalcMode(config.calcMode)
const variables = Array.isArray(config.variables) ? config.variables : []
if (calcMode === 'max' || calcMode === 'min') {
const labels = variables.map((variable) => wrapFieldTokenLabel(variable.label || variable.prop))
const modeLabel = calcMode === 'max' ? '最大值' : '最小值'
return labels.length ? `${modeLabel}(${labels.join(', ')})` : modeLabel
}
const repaired = repairCalcConfigForEvaluation(config)
return expressionToDisplayText(repaired.expression, repaired.variables) || '--'
}
export type CalcCompareDetailValue = '是' | '否' | '--'
export interface CalcCompareDetailItem {
result: CalcCompareDetailValue
threshold: number
}
export interface CalcCompareDetails {
greater?: CalcCompareDetailItem
less?: CalcCompareDetailItem
equal?: CalcCompareDetailItem
}
function isCompareThresholdConfigured(value: number | null | undefined): value is number {
return value !== null && value !== undefined && Number.isFinite(value)
}
function normalizeCompareThreshold(value: number, precision: number): number {
const safePrecision = Number.isFinite(precision) ? Math.max(0, Math.min(20, Math.trunc(precision))) : 2
return Number(value.toFixed(safePrecision))
}
function resolveCompareBoolean(
result: number | null,
compareValue: number | null | undefined,
operator: 'greater' | 'less' | 'equal',
precision: number
): boolean {
if (!isCompareThresholdConfigured(compareValue)) return false
if (result === null || !Number.isFinite(result)) return false
const normalizedResult = normalizeCompareThreshold(result, precision)
const normalizedThreshold = normalizeCompareThreshold(compareValue, precision)
if (operator === 'greater') return normalizedResult > normalizedThreshold
if (operator === 'less') return normalizedResult < normalizedThreshold
return normalizedResult === normalizedThreshold
}
function resolveCompareDetail(
result: number | null,
compareValue: number | null | undefined,
operator: 'greater' | 'less' | 'equal',
precision: number
): CalcCompareDetailItem | undefined {
if (!isCompareThresholdConfigured(compareValue)) return undefined
const threshold = compareValue
if (result === null || !Number.isFinite(result)) {
return { result: '--', threshold }
}
const compareResult = resolveCompareBoolean(result, compareValue, operator, precision)
return { result: compareResult ? '是' : '否', threshold }
}
export function buildCalcCompareDetails(
result: number | null,
config: Pick<
FlmCalcConfig,
'compareGreaterValue' | 'compareLessValue' | 'compareEqualValue' | 'precision'
>
): CalcCompareDetails {
const precision = config.precision ?? 2
const details: CalcCompareDetails = {}
const greater = resolveCompareDetail(result, config.compareGreaterValue, 'greater', precision)
const less = resolveCompareDetail(result, config.compareLessValue, 'less', precision)
const equal = resolveCompareDetail(result, config.compareEqualValue, 'equal', precision)
if (greater !== undefined) details.greater = greater
if (less !== undefined) details.less = less
if (equal !== undefined) details.equal = equal
return details
}
function normalizeSubmitResultValue(value: number | null, precision: number): number | null {
if (value === null || !Number.isFinite(value)) return null
const safePrecision = Number.isFinite(precision) ? Math.max(0, Math.min(20, Math.trunc(precision))) : 2
return Number(value.toFixed(safePrecision))
}
export function buildCalcSubmitValue(
result: number | null,
config: Pick<
FlmCalcConfig,
'compareGreaterValue' | 'compareLessValue' | 'compareEqualValue' | 'precision' | 'prefix' | 'suffix'
>
): FlmCalcSubmitValue {
const precision = config.precision ?? 2
const submitValue: FlmCalcSubmitValue = {
result: normalizeSubmitResultValue(result, precision),
prefix: config.prefix ?? '',
suffix: config.suffix ?? ''
}
if (isCompareThresholdConfigured(config.compareGreaterValue)) {
submitValue.greaterValue = config.compareGreaterValue
submitValue.isGreater = resolveCompareBoolean(result, config.compareGreaterValue, 'greater', precision)
}
if (isCompareThresholdConfigured(config.compareLessValue)) {
submitValue.lessValue = config.compareLessValue
submitValue.isLess = resolveCompareBoolean(result, config.compareLessValue, 'less', precision)
}
if (isCompareThresholdConfigured(config.compareEqualValue)) {
submitValue.equalValue = config.compareEqualValue
submitValue.isEqual = resolveCompareBoolean(result, config.compareEqualValue, 'equal', precision)
}
return submitValue
}
function normalizeParsedSubmitValue(raw: Record<string, unknown>): FlmCalcSubmitValue | null {
const resultRaw = raw.result
let result: number | null = null
if (resultRaw === null || resultRaw === undefined || resultRaw === '') {
result = null
} else if (typeof resultRaw === 'number' && Number.isFinite(resultRaw)) {
result = resultRaw
} else {
const parsedResult = Number(String(resultRaw).trim())
if (!Number.isFinite(parsedResult)) return null
result = parsedResult
}
const submitValue: FlmCalcSubmitValue = {
result,
prefix: typeof raw.prefix === 'string' ? raw.prefix : '',
suffix: typeof raw.suffix === 'string' ? raw.suffix : ''
}
if (typeof raw.greaterValue === 'number' && Number.isFinite(raw.greaterValue)) {
submitValue.greaterValue = raw.greaterValue
submitValue.isGreater = raw.isGreater === true
}
if (typeof raw.lessValue === 'number' && Number.isFinite(raw.lessValue)) {
submitValue.lessValue = raw.lessValue
submitValue.isLess = raw.isLess === true
}
if (typeof raw.equalValue === 'number' && Number.isFinite(raw.equalValue)) {
submitValue.equalValue = raw.equalValue
submitValue.isEqual = raw.isEqual === true
}
return submitValue
}
export function parseFlmCalcModelValue(raw: unknown): FlmCalcSubmitValue | null {
if (raw === null || raw === undefined || raw === '') return null
if (typeof raw === 'number' && Number.isFinite(raw)) {
return { result: raw, prefix: '', suffix: '' }
}
if (typeof raw === 'string') {
const trimmed = raw.trim()
if (!trimmed) return null
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
return { result: Number(trimmed), prefix: '', suffix: '' }
}
try {
const parsed = JSON.parse(trimmed) as unknown
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return normalizeParsedSubmitValue(parsed as Record<string, unknown>)
}
} catch {
return null
}
return null
}
if (typeof raw === 'object' && !Array.isArray(raw)) {
return normalizeParsedSubmitValue(raw as Record<string, unknown>)
}
return null
}
<template>
<view class="flm-calc">
<text class="flm-calc__value">{{ displayText }}</text>
<text v-if="detailLines.length" class="flm-calc__detail-trigger" @tap="toggleDetail">ⓘ</text>
<view v-if="detailVisible && detailLines.length" class="flm-calc__detail-popover">
<text v-for="(line, index) in detailLines" :key="index" class="flm-calc__detail-line">{{ line }}</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, inject, ref, watch } from 'vue'
import type { FlmCalcConfig, FlmCalcSubmitValue } from 'flame-uni-types'
import { flmCalcContextKey } from './flmCalcContext'
import {
buildCalcCompareDetails,
buildCalcFormulaDescription,
buildCalcSubmitValue,
evaluateCalcValue,
formatCalcResult,
parseFlmCalcModelValue
} from './evaluateCalcExpression'
const props = withDefaults(
defineProps<{
config?: FlmCalcConfig
rowModel?: Record<string, unknown> | null
}>(),
{
config: () => ({}),
rowModel: null
}
)
const emit = defineEmits<{
'update:modelValue': [FlmCalcSubmitValue]
}>()
const formContext = inject(flmCalcContextKey, null)
const detailVisible = ref(false)
function readDependencyValue(prop: string): unknown {
if (props.rowModel && typeof props.rowModel === 'object') {
return props.rowModel[prop]
}
if (formContext) return formContext.getValue(prop)
return undefined
}
const resolvedConfig = computed(() => props.config || {})
const isReadonlyMode = computed(() => {
const config = resolvedConfig.value
return config.disabled === true || config.readonly === true
})
const savedSubmitValue = computed(() => {
const config = resolvedConfig.value
const raw = config.modelValue ?? config['model-value']
return parseFlmCalcModelValue(raw)
})
const calculatedValue = computed(() => {
const config = resolvedConfig.value
const variables = Array.isArray(config.variables) ? config.variables : []
if (!variables.length) return null
const rowModel = props.rowModel
if (rowModel && typeof rowModel === 'object') {
for (const variableItem of variables) {
void rowModel[variableItem.prop]
}
}
return evaluateCalcValue(config, readDependencyValue)
})
const resolvedValue = computed(() => {
if (isReadonlyMode.value && savedSubmitValue.value !== null) {
return savedSubmitValue.value.result
}
return calculatedValue.value
})
const displayText = computed(() => {
const config = resolvedConfig.value
const formatted = formatCalcResult(resolvedValue.value, config.precision ?? 2)
if (formatted === '--') return formatted
return `${config.prefix ?? ''}${formatted}${config.suffix ?? ''}`
})
const detailLines = computed(() => {
const config = resolvedConfig.value
const lines: string[] = []
const formulaDescription = buildCalcFormulaDescription(config)
const resultText = displayText.value
lines.push(`计算公式:${formulaDescription}`)
lines.push(`计算结果:${resultText}`)
const compareDetails = buildCalcCompareDetails(resolvedValue.value, config)
if (compareDetails.greater !== undefined) {
lines.push(`是否大于${compareDetails.greater.threshold}:${compareDetails.greater.result}`)
}
if (compareDetails.less !== undefined) {
lines.push(`是否小于${compareDetails.less.threshold}:${compareDetails.less.result}`)
}
if (compareDetails.equal !== undefined) {
lines.push(`是否等于${compareDetails.equal.threshold}:${compareDetails.equal.result}`)
}
return lines
})
function isSameSubmitValue(left: FlmCalcSubmitValue, right: FlmCalcSubmitValue): boolean {
return JSON.stringify(left) === JSON.stringify(right)
}
function toggleDetail() {
detailVisible.value = !detailVisible.value
}
watch(
calculatedValue,
(nextValue) => {
if (isReadonlyMode.value && savedSubmitValue.value !== null) return
const submitValue = buildCalcSubmitValue(nextValue, resolvedConfig.value)
const currentSubmitValue = parseFlmCalcModelValue(
resolvedConfig.value.modelValue ?? resolvedConfig.value['model-value']
)
if (currentSubmitValue && isSameSubmitValue(currentSubmitValue, submitValue)) return
emit('update:modelValue', submitValue)
},
{ immediate: true }
)
</script>
<style scoped>
.flm-calc {
min-height: 72rpx;
line-height: 1.4;
color: #323233;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
width: 100%;
position: relative;
}
.flm-calc__value {
flex: 0 1 auto;
min-width: 0;
font-size: 28rpx;
}
.flm-calc__detail-trigger {
flex-shrink: 0;
font-size: 28rpx;
color: #969799;
padding: 4rpx 8rpx;
}
.flm-calc__detail-popover {
width: 100%;
margin-top: 8rpx;
padding: 16rpx;
background: #f7f8fa;
border-radius: 8rpx;
box-sizing: border-box;
}
.flm-calc__detail-line {
display: block;
font-size: 24rpx;
color: #646566;
line-height: 1.6;
white-space: pre-wrap;
}
</style>
import type { InjectionKey } from 'vue'
export interface FlmCalcContextValue {
getValue: (prop: string) => unknown
}
export const flmCalcContextKey: InjectionKey<FlmCalcContextValue> = Symbol('flmCalcContext')
export const flmCalcRowContextKey: InjectionKey<FlmCalcContextValue> = Symbol('flmCalcRowContext')
+1
-0
export { installFlameUni } from './uni_modules/flame-uni/register.js'
export { flameRequestContextKey } from './uni_modules/flame-uni/common/flameRequestContext.ts'
+8
-3
{
"name": "flame-uni",
"version": "0.0.4",
"version": "0.0.5",
"description": "基于 uni-app 的移动端动态表单组件库(与 flame-plus flmDynamicForm 配置协议对齐)",
"keywords": ["uni-app", "uni-ui", "flame", "dynamic-form"],
"keywords": [
"uni-app",
"uni-ui",
"flame",
"dynamic-form"
],
"license": "MIT",

@@ -39,3 +44,3 @@ "private": false,

"@vue/runtime-core": "^3.4.21",
"flame-uni-types": "file:../flame-uni-types",
"flame-uni-types": "1.0.1",
"typescript": "~5.4.5",

@@ -42,0 +47,0 @@ "vue": "^3.4.21",

@@ -1,5 +0,16 @@

import type { App } from 'vue'
import type { App, InjectionKey } from 'vue'
export function installFlameUni(app: App): void
export type FlameRequestFn = (params: {
projectName?: string
tableName: string
flameMethod: string
data?: Record<string, unknown>
service?: string
suffix?: string
}) => Promise<unknown>
export const flameRequestContextKey: InjectionKey<FlameRequestFn>
export { installFlameUni as default }
<template>
<view :id="idAttr" class="flm-date-picker">
<view
:id="idAttr"
class="flm-date-picker"
:class="{ 'flm-field-disabled': fieldDisabled }"
>
<!-- #ifdef H5 -->

@@ -37,2 +41,3 @@ <input

import type { DatePickerConfig } from 'flame-uni-types'
import { resolveDatePickerValueFormat } from '../../../common/datePickerValueFormat'

@@ -217,6 +222,10 @@ function pad(n: number) {

const hideSecond = computed(() => {
const fmt = cfg.value.valueFormat || cfg.value['value-format']
const fmt = resolvedValueFormat.value
return fmt ? !String(fmt).includes('ss') : true
})
const resolvedValueFormat = computed(() =>
resolveDatePickerValueFormat(dateType.value, cfg.value.valueFormat ?? cfg.value['value-format'])
)
const idAttr = computed(() => {

@@ -301,3 +310,3 @@ const id = cfg.value.id

function onUpdate(val: string | string[]) {
const fmt = cfg.value.valueFormat || cfg.value['value-format']
const fmt = resolvedValueFormat.value
if (isRange.value) {

@@ -335,6 +344,12 @@ let pair: string[] = []

<style scoped>
<style scoped lang="scss">
@import '../../../common/fieldDisabled.scss';
.flm-date-picker {
width: 100%;
}
.flm-field-disabled {
@include flm-field-disabled-deep;
}
</style>

@@ -11,2 +11,8 @@ <template>

/>
<flm-calc
v-else-if="matchType(formItem, 'flmCalc')"
:config="mergedConfig"
:row-model="rowModel"
@update:model-value="emitVal"
/>
<flm-input

@@ -145,2 +151,3 @@ v-else-if="matchType(formItem, 'flmInput')"

import FlmAlert from '../flmAlert/flmAlert.vue'
import FlmCalc from '../flmCalc/flmCalc.vue'
import FlmCascader from '../flmCascader/flmCascader.vue'

@@ -178,2 +185,3 @@ import FlmCheckbox from '../flmCheckbox/flmCheckbox.vue'

noLabel?: boolean
rowModel?: Record<string, unknown> | null
}>(),

@@ -185,3 +193,4 @@ {

mode: 'edit',
noLabel: false
noLabel: false,
rowModel: null
}

@@ -229,3 +238,4 @@ )

!matchType(formItem.value, 'flmRead') &&
!matchType(formItem.value, 'flmImage')
!matchType(formItem.value, 'flmImage') &&
!matchType(formItem.value, 'flmCalc')
)

@@ -242,2 +252,3 @@

if (controlType === 'flmTime') return ''
if (controlType === 'flmCalc') return null
if (controlType === 'flmCheckbox') {

@@ -274,2 +285,5 @@ if (isFlmCheckboxSingleModeForFormItem(item)) return false

}
if (controlTypeOf(formItemRecord) === 'flmTime' && effectiveMode.value !== 'edit') {
controlConfig.staticDisplay = true
}
if (

@@ -325,2 +339,6 @@ controlTypeOf(formItemRecord) === 'flmCheckbox' &&

}
if (controlType === 'flmCalc') {
if (value === undefined || value === null) return defaultForItem(item)
return value
}
if (controlType === 'flmInputNumber' || controlType === 'flmSlider' || controlType === 'flmRate') {

@@ -327,0 +345,0 @@ if (value === undefined || value === null) return defaultForItem(item)

<template>
<view
class="flm-input-wrap"
:class="{ 'flm-input-wrap--readonly': isReadonly }"
:class="{
'flm-input-wrap--readonly': isReadonly,
'flm-input-wrap--disabled': effectiveDisabled
}"
:style="wrapStyle"

@@ -188,3 +191,3 @@ >

backgroundColor: '#fff',
disableColor: '#F7F6F6',
disableColor: '#f7f8fa',
borderColor: '#e5e5e5'

@@ -250,3 +253,5 @@ }

<style scoped>
<style scoped lang="scss">
@import '../../../common/fieldDisabled.scss';
.flm-input-wrap {

@@ -278,2 +283,6 @@ width: 100%;

}
.flm-input-wrap--disabled {
@include flm-field-disabled-deep;
}
</style>

@@ -54,6 +54,6 @@ <template>

const readStyles = computed(() => ({
color: '#323233',
disableColor: '#F7F6F6',
color: '#646566',
disableColor: '#f7f8fa',
borderColor: 'transparent',
backgroundColor: '#F7F6F6'
backgroundColor: '#f7f8fa'
}))

@@ -67,9 +67,9 @@ </script>

.flmRead-wrap ::v-deep(.uni-easyinput__content.is-disabled) {
background-color: #f7f6f6 !important;
color: #323233;
background-color: #f7f8fa !important;
color: #646566;
}
.flmRead-wrap ::v-deep(.uni-easyinput__content-input) {
color: #323233 !important;
-webkit-text-fill-color: #323233;
color: #646566 !important;
-webkit-text-fill-color: #646566;
}
</style>
<template>
<view class="flmSearchSelect">
<template v-if="!disabled">
<template v-if="remoteMode">
<view class="flm-ss-remote-trigger" @tap="openRemotePicker">
<text class="flm-ss-remote-text" :class="{ 'is-placeholder': !displayLabel }">
{{ displayLabel || placeholderText }}
</text>
<text v-if="!disabled" class="flm-ss-remote-btn">选择</text>
</view>
<uni-popup ref="popupRef" type="bottom" background-color="#fff" @change="onPopupChange">
<view class="flm-ss-popup">
<view class="flm-ss-popup-head">
<text class="flm-ss-popup-cancel" @tap="closeRemotePicker">取消</text>
<text class="flm-ss-popup-title">请选择</text>
<text class="flm-ss-popup-confirm" @tap="confirmRemotePicker">确定</text>
</view>
<view class="flm-ss-popup-search">
<uni-easyinput
v-model="remoteKeyword"
type="text"
placeholder="关键字搜索"
:input-border="true"
@confirm="loadRemoteRows(true)"
/>
<text class="flm-ss-popup-search-btn" @tap="loadRemoteRows(true)">搜索</text>
</view>
<scroll-view scroll-y class="flm-ss-popup-scroll">
<view v-if="remoteLoading" class="flm-ss-popup-loading">加载中...</view>
<view
v-for="(row, rowIndex) in remoteRows"
:key="rowIndex"
class="flm-ss-popup-row"
:class="{ 'is-selected': isRemoteRowSelected(row) }"
@tap="toggleRemoteRow(row)"
>
<text>{{ remoteRowLabel(row) }}</text>
</view>
<view v-if="!remoteLoading && !remoteRows.length" class="flm-ss-popup-empty">暂无数据</view>
</scroll-view>
<view v-if="remoteHasMore && !remoteLoading" class="flm-ss-popup-more" @tap="loadRemoteRows(false)">
加载更多
</view>
</view>
</uni-popup>
</template>
<template v-else-if="!disabled">
<uni-easyinput
:model-value="keyword"
type="text"
:placeholder="placeholder"
:placeholder="placeholderText"
:disabled="false"
:input-border="true"
@update:model-value="onKw"
@update:model-value="onKeywordInput"
/>

@@ -15,8 +58,8 @@ <scroll-view scroll-y class="flm-ss-scroll">

<uni-list-item
v-for="(o, i) in filtered"
:key="i"
:title="o.label"
v-for="(option, optionIndex) in filtered"
:key="optionIndex"
:title="option.label"
:clickable="true"
:class="{ 'is-active': isActive(o) }"
@click="pick(o)"
:class="{ 'is-active': isActive(option) }"
@click="pickLocal(option)"
/>

@@ -28,3 +71,3 @@ </uni-list>

<uni-list v-else border>
<uni-list-item :title="displayLabel" />
<uni-list-item :title="displayLabel || '—'" />
</uni-list>

@@ -35,12 +78,13 @@ </view>

<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import type { SearchSelectConfig } from 'flame-uni-types'
import { flameRequestContextKey } from '../../../common/flameRequestContext'
const props = withDefaults(
defineProps<{
modelValue?: string | number | boolean
modelValue?: string | number | boolean | unknown[] | Record<string, unknown> | null
config?: SearchSelectConfig
}>(),
{
modelValue: '',
modelValue: undefined,
config: () => ({})

@@ -51,26 +95,95 @@ }

const emit = defineEmits<{
'update:modelValue': [string | number | boolean]
'update:modelValue': [unknown]
change: [unknown]
}>()
const disabled = computed(() => Boolean(props.config.disabled))
const flameRequest = inject(flameRequestContextKey, null)
const popupRef = ref<{ open?: (type?: string) => void; close?: () => void } | null>(null)
const placeholder = computed(() => (props.config.placeholder as string) || '输入关键字筛选')
const cfg = computed(() => props.config || {})
const disabled = computed(() => Boolean(cfg.value.disabled))
const placeholderText = computed(() => String(cfg.value.placeholder || '请选择'))
const remoteMode = computed(() => {
const tableName = cfg.value.fk_table_name
return tableName != null && String(tableName).trim() !== ''
})
const multipleSelect = computed(() => cfg.value.isMultipleSelect === true)
const valueFieldName = computed(() => String(cfg.value.fk_table_name_field || 'flame_id'))
const keyword = ref('')
const remoteKeyword = ref('')
const remoteRows = ref<Record<string, unknown>[]>([])
const remoteLoading = ref(false)
const remotePage = ref(1)
const remoteHasMore = ref(false)
const remoteSelectedRows = ref<Record<string, unknown>[]>([])
const localDisplayLabel = ref('')
const options = computed(() => {
const raw = props.config.options || props.config.items
if (!Array.isArray(raw)) return [] as { label: string; value: unknown; disabled: boolean }[]
return raw.map((o: Record<string, unknown>) => ({
label: o.label != null ? String(o.label) : String(o.value ?? ''),
value: o.value,
disabled: Boolean(o.disabled)
const raw = cfg.value.options ?? cfg.value.items
if (!Array.isArray(raw)) return [] as { label: string; value: unknown }[]
return raw.map((item) => ({
label: item.label != null ? String(item.label) : String(item.value ?? ''),
value: item.value
}))
})
const keyword = ref('')
const filtered = computed(() => {
const query = keyword.value.trim().toLowerCase()
if (!query) return options.value
return options.value.filter((option) => option.label.toLowerCase().includes(query))
})
function resolveDisplayFromModelValue(modelValue: unknown) {
if (modelValue == null || modelValue === '') {
localDisplayLabel.value = ''
return
}
if (multipleSelect.value && Array.isArray(modelValue)) {
if (!modelValue.length) {
localDisplayLabel.value = ''
return
}
const first = modelValue[0] as Record<string, unknown>
const displayKey = cfg.value.fk_table_display_field_name
? String(cfg.value.fk_table_display_field_name)
: ''
if (displayKey && displayKey in first) {
const label = String(first[displayKey])
localDisplayLabel.value =
modelValue.length > 1 ? `${label}等${modelValue.length}个条件` : label
return
}
localDisplayLabel.value = `${modelValue.length}项已选`
return
}
const refObject = cfg.value.ref as Record<string, unknown> | undefined
const displayKey = cfg.value.fk_table_display_field_name
? String(cfg.value.fk_table_display_field_name)
: ''
if (refObject && displayKey && displayKey in refObject) {
const refLabel = refObject[displayKey]
if (refLabel != null && refLabel !== '' && refLabel !== 0) {
localDisplayLabel.value = String(refLabel)
return
}
}
if (typeof modelValue === 'object' && modelValue !== null && displayKey) {
const record = modelValue as Record<string, unknown>
if (displayKey in record) {
localDisplayLabel.value = String(record[displayKey])
return
}
}
const hit = options.value.find((option) => Object.is(option.value, modelValue))
localDisplayLabel.value = hit ? hit.label : String(modelValue)
}
watch(
() => [props.modelValue, options.value],
() => [props.modelValue, cfg.value] as const,
() => {
const hit = options.value.find((o) => o.value === props.modelValue)
keyword.value = hit ? String(hit.label) : ''
resolveDisplayFromModelValue(props.modelValue)
},

@@ -80,26 +193,140 @@ { immediate: true, deep: true }

const filtered = computed(() => {
const k = keyword.value.trim().toLowerCase()
if (!k) return options.value
return options.value.filter((o) => String(o.label).toLowerCase().includes(k))
})
const displayLabel = computed(() => localDisplayLabel.value)
const displayLabel = computed(() => {
const hit = options.value.find((o) => o.value === props.modelValue)
return hit ? hit.label : String(props.modelValue ?? '')
})
function invokeChange(value: unknown) {
emit('update:modelValue', value)
emit('change', value)
const callback = cfg.value.onChange
if (typeof callback === 'function') callback(value)
}
function isActive(o: { value: unknown }) {
return o.value === props.modelValue
function isActive(option: { label: string; value: unknown }) {
return Object.is(option.value, props.modelValue)
}
function onKw(v: string) {
keyword.value = v ?? ''
function onKeywordInput(value: string | number) {
keyword.value = String(value ?? '')
}
function pick(o: { label: string; value: unknown; disabled: boolean }) {
if (o.disabled) return
keyword.value = String(o.label)
emit('update:modelValue', o.value as string | number | boolean)
function pickLocal(option: { label: string; value: unknown }) {
localDisplayLabel.value = option.label
invokeChange(option.value)
}
function remoteRowLabel(row: Record<string, unknown>) {
const displayKey = String(cfg.value.fk_table_display_field_name || 'name')
if (row[displayKey] != null) return String(row[displayKey])
const valueKey = valueFieldName.value
if (row[valueKey] != null) return String(row[valueKey])
return '—'
}
function isRemoteRowSelected(row: Record<string, unknown>) {
const valueKey = valueFieldName.value
const rowValue = row[valueKey]
return remoteSelectedRows.value.some((selected) => Object.is(selected[valueKey], rowValue))
}
function toggleRemoteRow(row: Record<string, unknown>) {
const valueKey = valueFieldName.value
const rowValue = row[valueKey]
if (multipleSelect.value) {
const exists = remoteSelectedRows.value.findIndex((selected) =>
Object.is(selected[valueKey], rowValue)
)
if (exists >= 0) {
remoteSelectedRows.value = remoteSelectedRows.value.filter((_, index) => index !== exists)
} else {
remoteSelectedRows.value = [...remoteSelectedRows.value, row]
}
return
}
remoteSelectedRows.value = [row]
}
async function loadRemoteRows(resetPage: boolean) {
if (!flameRequest || !remoteMode.value) {
uni.showToast({ title: '未配置远程请求', icon: 'none' })
return
}
if (resetPage) remotePage.value = 1
remoteLoading.value = true
try {
const tableName = String(cfg.value.fk_table_name)
const projectName = String(cfg.value.service_name || 'tenant')
const pageSize = 20
const response = (await flameRequest({
projectName,
tableName,
flameMethod: 'pageSearch',
data: {
page_index: remotePage.value,
page_size: pageSize,
conditions: remoteKeyword.value.trim()
? { keyword: remoteKeyword.value.trim() }
: {}
}
})) as { items?: Record<string, unknown>[]; total?: number } | null
const items = Array.isArray(response?.items) ? response!.items! : []
if (resetPage) {
remoteRows.value = items
} else {
remoteRows.value = [...remoteRows.value, ...items]
}
const total = Number(response?.total ?? 0)
remoteHasMore.value = remoteRows.value.length < total
if (!resetPage && items.length) remotePage.value += 1
if (resetPage && items.length === pageSize) remotePage.value = 2
} catch {
if (resetPage) remoteRows.value = []
} finally {
remoteLoading.value = false
}
}
function openRemotePicker() {
if (disabled.value) return
remoteSelectedRows.value = []
remoteKeyword.value = ''
popupRef.value?.open?.('bottom')
loadRemoteRows(true)
}
function closeRemotePicker() {
popupRef.value?.close?.()
}
function onPopupChange(event: { show?: boolean }) {
if (event?.show === false) remoteSelectedRows.value = []
}
function confirmRemotePicker() {
const rows = remoteSelectedRows.value
if (!rows.length) {
closeRemotePicker()
return
}
const valueKey = valueFieldName.value
const displayKey = cfg.value.fk_table_display_field_name
if (multipleSelect.value) {
const values = rows.map((row) => row[valueFieldName.value])
const displayKey = cfg.value.fk_table_display_field_name
? String(cfg.value.fk_table_display_field_name)
: ''
if (displayKey && rows.length > 1) {
localDisplayLabel.value = `${rows[0][displayKey]}等${rows.length}个条件`
} else if (displayKey && rows.length === 1) {
localDisplayLabel.value = String(rows[0][displayKey])
}
invokeChange(values)
} else {
const row = rows[0]
const displayKey = cfg.value.fk_table_display_field_name
? String(cfg.value.fk_table_display_field_name)
: ''
if (displayKey) localDisplayLabel.value = String(row[displayKey] ?? '')
invokeChange(row[valueFieldName.value])
}
closeRemotePicker()
}
</script>

@@ -109,8 +336,7 @@

.flmSearchSelect {
display: flex;
flex-direction: column;
gap: 12rpx;
width: 100%;
}
.flm-ss-scroll {
max-height: 280rpx;
max-height: 360rpx;
margin-top: 12rpx;
}

@@ -120,10 +346,91 @@ .flm-ss-empty {

color: #969799;
padding: 16rpx;
padding: 16rpx 0;
}
</style>
<style>
.flmSearchSelect :deep(.is-active .uni-list-item__container) {
background: #f0f7ff;
.flm-ss-remote-trigger {
display: flex;
flex-direction: row;
align-items: center;
min-height: 72rpx;
padding: 12rpx 16rpx;
border: 1px solid #ebedf0;
border-radius: 8rpx;
background: #fff;
}
.flm-ss-remote-text {
flex: 1;
font-size: 28rpx;
color: #323233;
}
.flm-ss-remote-text.is-placeholder {
color: #c8c9cc;
}
.flm-ss-remote-btn {
flex-shrink: 0;
font-size: 28rpx;
color: #2979ff;
margin-left: 16rpx;
}
.flm-ss-popup {
max-height: 80vh;
display: flex;
flex-direction: column;
}
.flm-ss-popup-head {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 24rpx;
border-bottom: 1px solid #ebedf0;
}
.flm-ss-popup-cancel,
.flm-ss-popup-confirm {
font-size: 28rpx;
color: #2979ff;
min-width: 80rpx;
}
.flm-ss-popup-title {
font-size: 30rpx;
font-weight: 600;
color: #323233;
}
.flm-ss-popup-search {
display: flex;
flex-direction: row;
align-items: center;
gap: 12rpx;
padding: 16rpx 24rpx;
}
.flm-ss-popup-search-btn {
flex-shrink: 0;
font-size: 28rpx;
color: #2979ff;
}
.flm-ss-popup-scroll {
flex: 1;
max-height: 50vh;
padding: 0 24rpx;
}
.flm-ss-popup-row {
padding: 24rpx 0;
border-bottom: 1px solid #f2f3f5;
font-size: 28rpx;
color: #323233;
}
.flm-ss-popup-row.is-selected {
color: #2979ff;
}
.flm-ss-popup-loading,
.flm-ss-popup-empty {
text-align: center;
padding: 32rpx;
font-size: 26rpx;
color: #969799;
}
.flm-ss-popup-more {
text-align: center;
padding: 24rpx;
font-size: 28rpx;
color: #2979ff;
}
</style>

@@ -7,3 +7,2 @@ <template>

<template v-if="multipleEnabled">
<text v-if="multipleSummary" class="flm-select-multiple-summary">{{ multipleSummary }}</text>
<view v-if="filterableEnabled" class="flm-select-search">

@@ -23,13 +22,77 @@ <uni-easyinput

<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 class="uni-data-tree flm-select-multiple-tree">
<view v-if="showLoading" class="flm-select-loading-mask">
<text class="flm-select-loading-text">{{ loadingText }}</text>
</view>
<view class="uni-data-tree-input" @click="handleMultipleInput">
<view class="input-value" :class="{ 'input-value-border': borderEnabled }">
<scroll-view
v-if="multipleDisplayText"
class="selected-area"
scroll-x
:show-scrollbar="false"
>
<text class="text-color">{{ multipleDisplayText }}</text>
</scroll-view>
<text v-else class="selected-area placeholder">{{ placeholderText }}</text>
<view
v-if="clearable && !pickerReadonly && multipleBindKeys.length"
class="icon-clear"
@click.stop="clearMultiple"
>
<uni-icons type="clear" color="#c0c4cc" size="24" />
</view>
<view
v-if="(!clearable || !multipleBindKeys.length) && !pickerReadonly"
class="arrow-area"
>
<view class="input-arrow" />
</view>
</view>
</view>
<view
v-if="multiplePopupOpened"
class="uni-data-tree-cover"
@click="closeMultiplePopup(true)"
/>
<view v-if="multiplePopupOpened" class="uni-data-tree-dialog">
<view class="uni-popper__arrow" />
<view class="dialog-caption">
<view class="title-area">
<text class="dialog-title">{{ popupTitleText }}</text>
</view>
<view class="dialog-close" @click="closeMultiplePopup(true)">
<view class="dialog-close-plus" />
<view class="dialog-close-plus dialog-close-rotate" />
</view>
</view>
<view class="flm-select-multiple-popup-body">
<scroll-view scroll-y class="flm-select-multiple-popup-scroll">
<view class="flm-select-multiple-popup-inner">
<uni-data-checkbox
multiple
mode="list"
:localdata="localdataForCheckbox"
:model-value="stagingKeys"
@update:model-value="onStagingKeysUpdate"
/>
<view
v-if="!localdataForCheckbox.length && !showLoading"
class="flm-select-popup-empty"
>
{{ optionsAfterRemote.length === 0 ? noDataText : noMatchText }}
</view>
</view>
</scroll-view>
</view>
<view class="flm-select-multiple-actions">
<text
class="flm-select-multiple-action flm-select-multiple-action--confirm"
@tap="confirmMultiplePopup"
>
确定
</text>
</view>
</view>
</view>
<uni-data-checkbox
multiple
mode="list"
:localdata="localdataForCheckbox"
:model-value="multipleBindKeys"
:disabled="pickerReadonly"
@update:model-value="onMultipleUpdate"
/>
</template>

@@ -157,3 +220,9 @@ <template v-else>

const wrapClasses = computed(() => [sizeClass.value, effectClass.value].filter(Boolean))
const wrapClasses = computed(() =>
[
sizeClass.value,
effectClass.value,
pickerReadonly.value ? 'flm-select-wrap--disabled' : ''
].filter(Boolean)
)

@@ -208,2 +277,4 @@ const valueKeyStr = computed(() => {

const didDefaultFirst = ref(false)
const multiplePopupOpened = ref(false)
const stagingKeys = ref<string[]>([])

@@ -326,2 +397,4 @@ function optionPrimitive(optionRow: Record<string, unknown>): string | number | boolean {

const multipleDisplayText = computed(() => multipleSummary.value)
const pickerBindValue = computed(() => {

@@ -408,2 +481,3 @@ const v = effectiveModelValue.value

if (remoteEnabled.value) scheduleRemote(filterQuery.value)
if (multipleEnabled.value) return
if (

@@ -425,2 +499,37 @@ defaultFirstOption.value &&

function handleMultipleInput() {
if (pickerReadonly.value) return
openMultiplePopup()
}
function openMultiplePopup() {
stagingKeys.value = [...multipleBindKeys.value]
multiplePopupOpened.value = true
onPopupOpened()
}
function closeMultiplePopup(_discard?: boolean) {
multiplePopupOpened.value = false
onPopupClosed()
}
function confirmMultiplePopup() {
onMultipleUpdate(stagingKeys.value)
multiplePopupOpened.value = false
onPopupClosed()
}
function clearMultiple() {
emitValue([])
}
function onStagingKeysUpdate(keys: string[] | string) {
const list = Array.isArray(keys) ? keys : keys ? [keys] : []
if (multipleLimit.value > 0 && list.length > multipleLimit.value) {
stagingKeys.value = list.slice(0, multipleLimit.value)
return
}
stagingKeys.value = list
}
function onPopupClosed() {

@@ -434,2 +543,6 @@ emit('visible-change', false)

emit('change', raw as string | number | boolean | unknown[])
const callback = cfg.value.onChange
if (typeof callback === 'function') {
callback(raw as string | number | boolean | object)
}
}

@@ -504,3 +617,5 @@

<style scoped>
<style scoped lang="scss">
@import '../../../common/fieldDisabled.scss';
.flm-select-wrap {

@@ -511,9 +626,205 @@ width: 100%;

.flm-select-multiple-summary {
display: block;
.flm-select-wrap--disabled {
@include flm-field-disabled-deep;
.flm-select-multiple-tree .input-value-border {
background-color: $flm-field-disabled-bg;
}
.flm-select-multiple-tree .text-color,
.flm-select-multiple-tree .placeholder {
color: $flm-field-disabled-text;
}
}
.flm-select-multiple-tree {
position: relative;
font-size: 14px;
}
.flm-select-multiple-tree .input-value {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
font-size: 14px;
padding: 0 10px;
padding-right: 5px;
overflow: hidden;
height: 35px;
box-sizing: border-box;
}
.flm-select-multiple-tree .input-value-border {
border: 1px solid #e5e5e5;
border-radius: 5px;
}
.flm-select-multiple-tree .selected-area {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: row;
}
.flm-select-multiple-tree .text-color {
color: #333;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flm-select-multiple-tree .placeholder {
color: grey;
font-size: 12px;
}
.flm-select-multiple-tree .arrow-area {
position: relative;
width: 20px;
margin-bottom: 5px;
margin-left: auto;
display: flex;
justify-content: center;
transform: rotate(-45deg);
transform-origin: center;
}
.flm-select-multiple-tree .input-arrow {
width: 7px;
height: 7px;
border-left: 1px solid #999;
border-bottom: 1px solid #999;
}
.flm-select-multiple-tree .icon-clear {
display: flex;
align-items: center;
}
.flm-select-multiple-tree .uni-data-tree-cover {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
z-index: 100;
}
.flm-select-multiple-tree .uni-data-tree-dialog {
position: fixed;
left: 0;
top: 20%;
right: 0;
bottom: 0;
background-color: #ffffff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
display: flex;
flex-direction: column;
z-index: 102;
overflow: hidden;
}
.flm-select-multiple-tree .uni-popper__arrow {
display: none;
}
.flm-select-multiple-tree .dialog-caption {
position: relative;
display: flex;
flex-direction: row;
flex-shrink: 0;
background-color: #ffffff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
overflow: hidden;
}
.flm-select-multiple-tree .title-area {
display: flex;
align-items: center;
margin: auto;
padding: 0 10px;
}
.flm-select-multiple-tree .dialog-title {
line-height: 44px;
font-size: 16px;
color: #333;
}
.flm-select-multiple-tree .dialog-close {
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 15px;
}
.flm-select-multiple-tree .dialog-close-plus {
width: 16px;
height: 2px;
background-color: #666;
border-radius: 2px;
transform: rotate(45deg);
}
.flm-select-multiple-tree .dialog-close-rotate {
position: absolute;
transform: rotate(-45deg);
}
.flm-select-multiple-popup-body {
flex: 1;
min-height: 0;
overflow: hidden;
}
.flm-select-multiple-popup-scroll {
height: 100%;
}
.flm-select-multiple-popup-inner {
padding: 0 15px;
box-sizing: border-box;
}
.flm-select-popup-empty {
padding: 24rpx 0;
font-size: 26rpx;
color: #323233;
margin-bottom: 12rpx;
color: #969799;
text-align: center;
}
.flm-select-multiple-actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 16rpx 32rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
border-top: 1px solid #f0f0f0;
background: #fff;
}
.flm-select-multiple-action {
font-size: 28rpx;
line-height: 1.4;
padding: 8rpx 0;
}
.flm-select-multiple-action--confirm {
color: #2979ff;
font-weight: 600;
}
.flm-select-search {

@@ -565,2 +876,3 @@ margin-bottom: 12rpx;

.flm-select--effect-dark .uni-data-tree-dialog,
.flm-select--effect-dark ::v-deep(.uni-data-tree-dialog) {

@@ -570,2 +882,3 @@ background-color: #1e1e1e;

.flm-select--effect-dark .dialog-title,
.flm-select--effect-dark ::v-deep(.dialog-title) {

@@ -575,2 +888,3 @@ color: #e5e5e5;

.flm-select--effect-dark .text-color,
.flm-select--effect-dark ::v-deep(.text-color) {

@@ -580,5 +894,75 @@ color: #e5e5e5;

.flm-select--effect-dark .placeholder,
.flm-select--effect-dark ::v-deep(.placeholder) {
color: #888;
}
.flm-select--effect-dark .flm-select-multiple-tree .dialog-caption {
background-color: #1e1e1e;
}
.flm-select--effect-dark .flm-select-multiple-actions {
background-color: #1e1e1e;
border-top-color: #333;
}
.flm-select--effect-dark .flm-select-multiple-action--confirm {
color: #409eff;
}
/* #ifdef H5 */
@media all and (min-width: 768px) {
.flm-select-multiple-tree .uni-data-tree-cover {
background-color: transparent;
}
.flm-select-multiple-tree .uni-data-tree-dialog {
position: absolute;
top: 55px;
height: auto;
min-height: 400px;
max-height: 50vh;
background-color: #fff;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
overflow: unset;
}
.flm-select-multiple-tree .dialog-caption {
display: flex;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.flm-select-multiple-tree .uni-popper__arrow,
.flm-select-multiple-tree .uni-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
}
.flm-select-multiple-tree .uni-popper__arrow {
display: block;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
top: -6px;
left: 10%;
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #ebeef5;
}
.flm-select-multiple-tree .uni-popper__arrow::after {
content: ' ';
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
}
/* #endif */
</style>

@@ -8,6 +8,6 @@ <template>

<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import type { FlmTimeConfig } from 'flame-uni-types'
import { resolveFlmTimeFormat } from '../../../common/flmTimeFormat'
import type { FlmTimeConfig, FlmTimeFormatPreset } from 'flame-uni-types'
const props = withDefaults(

@@ -28,3 +28,3 @@ defineProps<{

const cfg = computed(() => (props.config && typeof props.config === 'object' ? props.config : {}) as Record<string, unknown>)
const cfg = computed(() => (props.config && typeof props.config === 'object' ? props.config : {}) as FlmTimeConfig)

@@ -35,48 +35,76 @@ const now = ref(new Date())

function pad2(n: number) {
if (!Number.isFinite(n)) return '00'
return n < 10 ? `0${n}` : String(n)
function pad2(value: number) {
if (!Number.isFinite(value)) return '00'
return value < 10 ? `0${value}` : String(value)
}
function formatCustomDateTime(d: Date, formatStr: string) {
const trimmed = String(formatStr).trim() || 'YYYY-MM-DD HH:mm:ss'
function formatCustomDateTime(dateValue: Date, formatString: string) {
const trimmed = String(formatString).trim() || 'YYYY-MM-DD HH:mm:ss'
const segments = trimmed.split(/\s+/)
const datePart = segments[0] || 'YYYY-MM-DD'
const timePart = segments[1] || ''
const dateOut = datePart
.replace(/YYYY/g, String(d.getFullYear()))
.replace(/MM/g, pad2(d.getMonth() + 1))
.replace(/DD/g, pad2(d.getDate()))
if (!timePart) return dateOut
const timeOut = timePart
.replace(/HH/g, pad2(d.getHours()))
.replace(/mm/g, pad2(d.getMinutes()))
.replace(/ss/g, pad2(d.getSeconds()))
return `${dateOut} ${timeOut}`
const dateOutput = datePart
.replace(/YYYY/g, String(dateValue.getFullYear()))
.replace(/MM/g, pad2(dateValue.getMonth() + 1))
.replace(/DD/g, pad2(dateValue.getDate()))
if (!timePart) return dateOutput
const timeOutput = timePart
.replace(/HH/g, pad2(dateValue.getHours()))
.replace(/mm/g, pad2(dateValue.getMinutes()))
.replace(/ss/g, pad2(dateValue.getSeconds()))
return `${dateOutput} ${timeOutput}`
}
const resolvedFormat = computed(() => {
const c = cfg.value as FlmTimeConfig
const preset = (c.formatPreset ?? 'date') as FlmTimeFormatPreset
const custom = (c.format && String(c.format).trim()) || 'YYYY-MM-DD HH:mm:ss'
if (preset === 'date') return 'date:YYYY-MM-DD'
if (preset === 'time') return 'time:HH:mm:ss'
return `custom:${custom}`
const staticValue = computed(() => {
const config = cfg.value
const raw = props.modelValue ?? config.modelValue ?? config['model-value']
if (raw == null) return ''
return String(raw).trim()
})
const isViewDisabled = computed(() => {
const config = cfg.value
return config.disabled === true || config.readonly === true
})
const shouldShowStaticValue = computed(() => {
const config = cfg.value
if (config.staticDisplay === true) return true
return isViewDisabled.value && staticValue.value !== ''
})
const useLiveClock = computed(() => !shouldShowStaticValue.value)
const resolvedFormat = computed(() => resolveFlmTimeFormat(cfg.value.formatPreset, cfg.value.format))
const displayText = computed(() => {
const d = now.value
const rf = resolvedFormat.value
if (rf.startsWith('date:')) {
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
if (shouldShowStaticValue.value) return staticValue.value
const dateValue = now.value
const formatToken = resolvedFormat.value
if (formatToken.startsWith('date:')) {
return `${dateValue.getFullYear()}-${pad2(dateValue.getMonth() + 1)}-${pad2(dateValue.getDate())}`
}
if (rf.startsWith('time:')) {
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
if (formatToken.startsWith('time:')) {
return `${pad2(dateValue.getHours())}:${pad2(dateValue.getMinutes())}:${pad2(dateValue.getSeconds())}`
}
const custom = rf.slice('custom:'.length)
return formatCustomDateTime(d, custom)
const custom = formatToken.slice('custom:'.length)
return formatCustomDateTime(dateValue, custom)
})
function clearTimer() {
if (timer != null) {
clearInterval(timer)
timer = null
}
}
function startTimer() {
clearTimer()
timer = setInterval(() => {
now.value = new Date()
}, 1000)
}
function invokeCallbacks(text: string) {
const callback = (props.config as FlmTimeConfig | undefined)?.onChange
const callback = cfg.value.onChange
if (typeof callback === 'function') callback(text)

@@ -89,2 +117,3 @@ emit('update:modelValue', text)

(text) => {
if (!useLiveClock.value) return
invokeCallbacks(text)

@@ -95,13 +124,17 @@ },

onMounted(() => {
timer = setInterval(() => {
now.value = new Date()
}, 1000)
})
watch(
useLiveClock,
(live) => {
if (live) {
now.value = new Date()
startTimer()
} else {
clearTimer()
}
},
{ immediate: true }
)
onUnmounted(() => {
if (timer != null) {
clearInterval(timer)
timer = null
}
clearTimer()
})

@@ -108,0 +141,0 @@ </script>

<template>
<view :id="idAttr" class="flm-time-picker">
<view
:id="idAttr"
class="flm-time-picker"
:class="{ 'flm-field-disabled': fieldDisabled }"
>
<!-- #ifdef H5 -->

@@ -287,3 +291,5 @@ <input v-if="nameAttr && !isRange" type="hidden" :name="nameAttr" :value="nameValueStr" />

<style scoped>
<style scoped lang="scss">
@import '../../../common/fieldDisabled.scss';
.flm-time-picker {

@@ -293,2 +299,6 @@ width: 100%;

.flm-field-disabled {
@include flm-field-disabled-deep;
}
.flm-time-range {

@@ -295,0 +305,0 @@ display: flex;

<template>
<view :id="idAttr" class="flm-time-select">
<view
:id="idAttr"
class="flm-time-select"
:class="{ 'flm-field-disabled': fieldDisabled }"
>
<!-- #ifdef H5 -->

@@ -161,6 +165,12 @@ <input v-if="nameAttr" type="hidden" :name="nameAttr" :value="nameValueStr" />

<style scoped>
<style scoped lang="scss">
@import '../../../common/fieldDisabled.scss';
.flm-time-select {
width: 100%;
}
.flm-field-disabled {
@include flm-field-disabled-deep;
}
</style>

@@ -58,2 +58,3 @@ <template>

v-else-if="g.type === 'subForm'"
:ref="(el) => setSubFormRef(String(g.id), el)"
:config="subFormConfigFor(g)"

@@ -65,9 +66,17 @@ :model-value="subFormGet(g.id)"

</view>
<view v-if="safeButtons.length && showActionButtons" class="flm-actions">
<flm-button
<view
v-if="safeButtons.length && showActionButtons"
class="flm-actions"
:class="flmActionsLayoutClass"
>
<view
v-for="(btn, bi) in safeButtons"
:key="bi"
:config="buttonConfig(btn)"
@click="onButtonClick(btn)"
/>
class="flm-actions-item"
>
<flm-button
:config="buttonConfig(btn)"
@click="onButtonClick(btn)"
/>
</view>
</view>

@@ -78,3 +87,3 @@ </view>

<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { computed, provide, reactive, ref, watch } from 'vue'
import FlmAlert from '../../base/flmAlert/flmAlert.vue'

@@ -84,2 +93,3 @@ import FlmButton from '../../base/flmButton/flmButton.vue'

import FlmSubForm from '../flmSubForm/flmSubForm.vue'
import { flmCalcContextKey } from '../../base/flmCalc/flmCalcContext'
import { isFlmCheckboxSingleModeForFormItem } from '../../../common/checkboxMode'

@@ -218,2 +228,3 @@ import type {

if (controlType === 'flmTime') return ''
if (controlType === 'flmCalc') return null
if (controlType === 'flmCheckbox') {

@@ -313,2 +324,6 @@ if (isFlmCheckboxSingleModeForFormItem(item)) return false

const flmActionsLayoutClass = computed(() =>
safeButtons.value.length <= 3 ? 'flm-actions--row' : 'flm-actions--grid'
)
const collapseEnabled = computed(() => {

@@ -347,3 +362,63 @@ const root = props.config as Record<string, unknown> | undefined

const subFormState = reactive<Record<string, unknown[]>>({})
const subFormRefs = ref<Record<string, unknown>>({})
const hasExternalRecordValue = computed(() => {
const recordValue = props.value
return recordValue != null && typeof recordValue === 'object' && Object.keys(recordValue).length > 0
})
function isFlmTimeItem(item: FormItem) {
return controlTypeOf(item) === 'flmTime'
}
function patchFlmTimeControlConfig(controlConfig: unknown) {
const baseConfig =
controlConfig && typeof controlConfig === 'object' && !Array.isArray(controlConfig)
? { ...(controlConfig as Record<string, unknown>) }
: {}
return { ...baseConfig, disabled: true, staticDisplay: true }
}
provide(flmCalcContextKey, {
getValue: (prop: string) => {
for (const group of safeGroups.value) {
if (group.type !== 'form' || group.id == null) continue
const row = formState[String(group.id)]
if (row && Object.prototype.hasOwnProperty.call(row, prop)) {
return row[prop]
}
}
return undefined
}
})
function setSubFormRef(groupId: string, componentInstance: unknown) {
if (componentInstance) {
subFormRefs.value[groupId] = componentInstance
} else {
delete subFormRefs.value[groupId]
}
}
function pickSubFormRows(componentInstance: unknown): unknown[] | null {
if (!componentInstance || typeof componentInstance !== 'object') return null
const instanceRecord = componentInstance as Record<string, unknown>
const getRowsFn = instanceRecord.getRows
if (typeof getRowsFn !== 'function') return null
const rowsResult = getRowsFn()
return Array.isArray(rowsResult) ? rowsResult : null
}
function collectSubFormRows(groupId: string): unknown[] {
const subFormInstance = subFormRefs.value[groupId]
const rowsFromRef = pickSubFormRows(subFormInstance)
const list = rowsFromRef ?? subFormState[groupId] ?? []
if (!Array.isArray(list)) return []
try {
return JSON.parse(JSON.stringify(list))
} catch {
return [...list]
}
}
function groupKey(group: { id?: unknown }, index: number) {

@@ -366,3 +441,8 @@ return group.id != null ? String(group.id) : `g-${index}`

function patchItemForMode(item: FormItem): FormItem {
if (resolvedMode.value === 'edit') return item
if (resolvedMode.value === 'edit') {
if (hasExternalRecordValue.value && isFlmTimeItem(item)) {
return { ...item, controlConfig: patchFlmTimeControlConfig(item.controlConfig) }
}
return item
}
const controlConfig =

@@ -378,3 +458,7 @@ item.controlConfig && typeof item.controlConfig === 'object'

}
return { ...item, controlConfig }
const nextItem = { ...item, controlConfig }
if (isFlmTimeItem(item)) {
return { ...nextItem, controlConfig: patchFlmTimeControlConfig(item.controlConfig) }
}
return nextItem
}

@@ -558,7 +642,3 @@

} 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>) }))
: []
;(out as Record<string, unknown>)[String(group.id)] = collectSubFormRows(String(group.id))
}

@@ -663,6 +743,22 @@ })

display: flex;
flex-direction: column;
flex-direction: row;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 16rpx;
}
.flm-actions--row {
flex-wrap: nowrap;
}
.flm-actions--row .flm-actions-item {
flex: 1;
min-width: 0;
}
.flm-actions--grid .flm-actions-item {
flex: 0 0 calc((100% - 16rpx) / 2);
width: calc((100% - 16rpx) / 2);
box-sizing: border-box;
}
.flm-actions-item :deep(.flmButton) {
width: 100%;
}
.flmDynamicForm--read,

@@ -673,1 +769,10 @@ .flmDynamicForm--disabled {

</style>
<style scoped lang="scss">
@import '@/uni_modules/flame-uni/common/fieldDisabled.scss';
.flmDynamicForm--disabled,
.flmDynamicForm--read {
@include flm-field-disabled-deep;
}
</style>

@@ -43,2 +43,3 @@ <template>

:mode="subFormMode"
:row-model="row"
@update:model-value="(ev) => patchRowFromField(ri, it, ev)"

@@ -60,2 +61,6 @@ />

import { isFlmCheckboxSingleModeForFormItem } from '../../../common/checkboxMode'
import {
prefillSubFormRowFlmTimeFields,
updateSubFormRowFromModel
} from '../../../common/subFormRowHelpers'
import type { DynamicFormItemConfig, SubFormConfig } from 'flame-uni-types'

@@ -82,2 +87,3 @@

if (t === 'flmTime') return ''
if (t === 'flmCalc') return null
if (t === 'flmCheckbox') {

@@ -244,22 +250,33 @@ if (isFlmCheckboxSingleModeForFormItem(item)) return false

const row: Record<string, unknown> = {}
const base =
const baseModel =
props.config.model && typeof props.config.model === 'object'
? { ...(props.config.model as Record<string, unknown>) }
: {}
formItems.value.forEach((it) => {
if (typeof it.prop !== 'string') return
if (Object.prototype.hasOwnProperty.call(base, it.prop)) {
row[it.prop] = base[it.prop]
updateSubFormRowFromModel(row, baseModel, formItems.value, false)
formItems.value.forEach((item) => {
if (typeof item.prop !== 'string') return
if (Object.prototype.hasOwnProperty.call(row, item.prop) && row[item.prop] !== undefined) {
return
}
const cc = it.controlConfig
if (cc && typeof cc === 'object' && Object.prototype.hasOwnProperty.call(cc, 'modelValue')) {
row[it.prop] = cc.modelValue
const controlConfig = item.controlConfig
if (
controlConfig &&
typeof controlConfig === 'object' &&
Object.prototype.hasOwnProperty.call(controlConfig, 'modelValue')
) {
row[item.prop] = controlConfig.modelValue
return
}
row[it.prop] = defaultForItem(it)
row[item.prop] = defaultForItem(item)
})
prefillSubFormRowFlmTimeFields(row, formItems.value)
return row
}
function getRows() {
return internalList.value.map((row) => ({ ...row }))
}
defineExpose({ getRows })
function normalizeIncoming(rows: unknown) {

@@ -266,0 +283,0 @@ const list = Array.isArray(rows)

{
"id": "flame-uni",
"displayName": "flame-uni",
"version": "0.0.4",
"version": "0.0.5",
"description": "基于 uni-app 的动态表单组件,与 flame-plus flmDynamicForm 配置对齐",

@@ -6,0 +6,0 @@ "keywords": ["uni-app", "flame", "dynamic-form"],