| 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", |
+12
-1
@@ -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"], |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
235548
30.9%43
22.86%688
1428.89%1
Infinity%