| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662 |
- <template>
- <FlexView
- :touchable="touchable || childOnClickListener !== undefined"
- :pressedColor="themeContext.resolveThemeColor('FieldPressedColor', 'pressed.white')"
- :innerStyle="{
- ...themeStyles.field.value,
- ...(labelPosition === 'top' ? themeStyles.fieldVertical.value : {}),
- ...fieldStyle,
- ...(focused ? activeFieldStyle : {}),
- ...(error || finalErrorMessage ? errorFieldStyle : {})
- }"
- :direction="labelPosition === 'top' ? 'column' : 'row'"
- :center="labelPosition === 'top' ? false : true"
- @click="onClick"
- >
- <!-- 左边的标签区域 -->
- <FlexRow
- v-if="showLabel !== false && label"
- align="center"
- :flex="labelFlex"
- >
- <text v-if="requiredShow && showRequiredBadge" :style="themeStyles.requiredMark.value">*</text>
- <slot name="leftIcon" />
- <view v-if="!label" :style="labelStyle" />
- <text v-else :style="{
- ...themeStyles.labelText.value,
- width: labelWidth,
- textAlign: labelAlign,
- color: themeContext.resolveThemeColor(disabled ? labelDisableColor : labelColor),
- ...labelStyle,
- }">
- {{ label + (colon ? ': ' : '') }}
- </text>
- </FlexRow>
- <!-- 输入框区域 -->
- <FlexCol :flex="inputFlex">
- <FlexRow
- justify="space-between"
- align="center"
- :innerStyle="themeStyles.inputWapper2.value"
- >
- <slot name="prefix" />
- <slot name="leftButton" />
- <slot name="control" />
- <slot>
- <textarea
- v-if="multiline"
- ref="inputRef"
- :style="{
- ...themeStyles.input.value,
- ...inputStyle,
- ...(focused ? activeInputStyle : {}),
- color: themeContext.resolveThemeColor(disabled ? inputDisableColor : (error ? errorTextColor : inputColor)),
- textAlign: inputAlign,
- }"
- :autoHeight="autoHeight"
- :value="inputValue"
- :password="type==='password'"
- :placeholder="placeholder"
- :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
- confirm-type="done"
- :maxlength="maxLength"
- :disabled="disabled"
- :readonly="readonly"
- @input="onInput"
- @focus="onFocus"
- @blur="onBlur"
- @confirm="onBlur"
- @click="onClick"
- />
- <input
- v-else
- ref="inputRef"
- :style="{
- ...themeStyles.input.value,
- ...inputStyle,
- ...(focused ? activeInputStyle : {}),
- color: themeContext.resolveThemeColor(disabled ? inputDisableColor : (error ? errorTextColor : inputColor)),
- textAlign: inputAlign,
- }"
- :value="inputValue"
- :password="type==='password'"
- :placeholder="placeholder"
- :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
- confirm-type="done"
- :type="selectStyleType(type, 'text', {
- text: 'text',
- password: 'safe-password',
- number: 'number',
- decimal: 'digit',
- tel: 'tel',
- email: 'text',
- }) || 'text'"
- :maxlength="maxLength"
- :disabled="disabled"
- :readonly="readonly"
- @input="onInput"
- @focus="onFocus"
- @blur="onBlur"
- @confirm="onBlur"
- @click="onClick"
- />
- </slot>
- <slot name="suffix" />
- <slot name="rightButton" />
- <Icon v-if="showRightArrow" icon="arrow-right" :size="themeContext.resolveThemeSize('FieldRightArrowSize', 30)" v-bind="rightArrowProps" />
- </FlexRow>
- <FlexRow
- v-if="finalErrorMessage"
- :gap="10"
- :innerStyle="themeStyles.errorMessage.value"
- align="center"
- >
- <Icon
- :icon="errorIcon"
- :size="themeContext.resolveThemeSize('FieldErrorIconSize', 40)"
- v-bind="errorIconProps"
- :color="themeContext.resolveThemeColor('FieldErrorMessageColor', 'danger')"
- />
- <text :style="themeStyles.errorMessageText.value">{{finalErrorMessage}}</text>
- </FlexRow>
- <text v-if="showWordLimit" :style="themeStyles.wordLimitText.value">{{wordLimitText}}</text>
- </FlexCol>
- <!-- 清除按钮 -->
- <IconButton
- v-if="(clearButton && (
- clearButtonMode === 'always'
- || (clearButtonMode === 'while-editing' && focused))
- || (clearButtonMode === 'unless-editing' && modelValue !== '')
- )"
- icon="delete-filling"
- :color="themeContext.resolveThemeColor('FieldClearIconColor', 'grey')"
- v-bind="clearButtonProps"
- @click="onClear"
- />
- </FlexView>
- </template>
- <script setup lang="ts">
- import { computed, inject, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
- import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
- import { FormItemContextContextKey, propGetFormContext, type FormContext, type FormItemContext, type FormItemInternalContext } from './FormContext';
- import { DynamicColor, DynamicSize, DynamicVar, selectStyleType } from '../theme/ThemeTools';
- import type { IconProps } from '../basic/Icon.vue';
- import Icon from '../basic/Icon.vue';
- import IconButton from '../basic/IconButton.vue';
- import FlexCol from '../layout/FlexCol.vue';
- import FlexRow from '../layout/FlexRow.vue';
- import FlexView from '../layout/FlexView.vue';
- export interface FieldInstance {
- /**
- * 获取输入框焦点
- */
- focus: () => void;
- /**
- * 取消输入框焦点
- */
- blur: () => void;
- /**
- * 清空输入框
- */
- clear: () => void;
- /**
- * 返回值表明当前输入框是否获得了焦点。
- */
- isFocused: () => boolean;
- /**
- * 清除当前条目的校验状态
- */
- clearValidate: () => void;
- }
- export interface FieldProps {
- modelValue?: any,
- /**
- * 输入框左侧文本
- */
- label?: string;
- /**
- * 名称,作为提交表单时的标识符
- */
- name?: string;
- /**
- * 是否可点击.可以作为一个纯点击条目
- */
- touchable?: boolean;
- /**
- * 输入框类型
- * @default 'text'
- */
- type?: 'text'|'tel'|'number'|'password'|'number'|'email'|'decimal';
- /**
- * 输入的最大字符数
- */
- maxLength?: number;
- /**
- * 输入框占位提示文字
- */
- placeholder?: string;
- /**
- * 是否禁用输入框
- * @default false
- */
- disabled?: boolean;
- /**
- * 是否只读
- * @default false
- */
- readonly?: boolean;
- /**
- * 是否内容垂直居中
- * @default false
- */
- center?: boolean;
- /**
- * 是否在 label 后面添加冒号
- * @default true
- */
- colon?: boolean;
- /**
- * 是否必填
- * @default false
- */
- required?: boolean;
- /**
- * 多行文字
- * @default false
- */
- multiline?: boolean;
- /**
- * 多行文字下是否自动调整高度
- * @default true
- */
- autoHeight?: boolean;
- /**
- * 是否显示表单必填星号
- * @default true
- */
- showRequiredBadge?: boolean;
- /**
- * 是否启用清除图标,点击清除图标后会清空输入框
- * @default false
- */
- clearButton?: boolean;
- /**
- * 清除图标的自定义属性
- */
- clearButtonProps?: IconProps;
- /**
- * 清除图标的显示模式
- */
- clearButtonMode?: 'always'|'while-editing'|'unless-editing';
- /**
- * 左侧文本的宽度
- */
- labelWidth?: string|number;
- /**
- * 左侧文本对齐
- * @default 'left'
- */
- labelAlign?: 'left'|'center'|'right';
- /**
- * 左侧文本的位置
- * @default 'left'
- */
- labelPosition?: 'top'|'left';
- /**
- * 左侧文本的flex占比
- * @default undefined
- */
- labelFlex?: number;
- /**
- * 输入框的flex占比
- * @default 5
- */
- inputFlex?: number;
- /**
- * 左侧文本的样式
- */
- labelStyle?: TextStyle;
- /**
- * 左侧文本的颜色
- */
- labelColor?: string;
- /**
- * 左侧文本的禁用颜色
- */
- labelDisableColor?: string;
- /**
- * 输入框样式
- */
- inputStyle?: TextStyle;
- /**
- * 激活时的外壳样式
- */
- activeInputStyle?: TextStyle;
- /**
- * 输入框颜色
- */
- inputColor?: string;
- /**
- * 输入框文本对齐。
- * @default 'left'
- */
- inputAlign?: 'left'|'center'|'right';
- /**
- * 输入框禁用颜色
- */
- inputDisableColor?: string;
- /**
- * 外壳样式
- */
- fieldStyle?: ViewStyle;
- /**
- * 激活时的外壳样式
- */
- activeFieldStyle?: ViewStyle;
- /**
- * 错误时的外壳样式
- */
- errorFieldStyle?: ViewStyle;
- /**
- * 错误时的文字颜色
- * @default Color.danger
- */
- errorTextColor?: string;
- /**
- * 文本框水印文字颜色
- * @default Color.grey
- */
- placeholderTextColor?: string;
- /**
- * 输入内容格式化函数
- */
- formatter?: (text: string) => string;
- /**
- * 格式化函数触发的时机,可选值为 blur
- * @default 'input'
- */
- formatTrigger?: 'blur'|'input';
- /**
- * 设置字段校验的时机
- * * blur 文本框失去焦点时校验
- * * change 数值更改时校验
- * * submit 提交时校验(默认)
- * @default 'submit'
- */
- validateTrigger?: 'blur'|'change'|'submit';
- /**
- * 是否将输入内容标红。
- */
- error?: boolean;
- /**
- * 错误提示的图标
- * @default 'prompt'
- */
- errorIcon?: string;
- /**
- * 错误提示图标的自定义属性
- */
- errorIconProps?: IconProps;
- /**
- * 底部错误提示文案,为空时不展示
- */
- errorMessage?: string;
- /**
- * 点击输入框是否重置错误状态
- * @default true
- */
- resetErrorOnClick?: boolean;
- /**
- * 是否显示字数统计
- * @default false
- */
- showWordLimit?: boolean;
- /**
- * 是否显左边标题
- * @default true
- */
- showLabel?: boolean;
- /**
- * 是否显右边箭头
- * @default false
- */
- showRightArrow?: boolean;
- /**
- * 右边箭头的自定义属性
- */
- rightArrowProps?: IconProps;
- }
- defineOptions({
- options: {
- styleIsolation: "shared",
- virtualHost: true,
- }
- })
- const emit = defineEmits([ 'update:modelValue', 'click', 'blur', 'focus', 'clear' ])
- const props = withDefaults(defineProps<FieldProps>(), {
- label: '',
- labelColor: () => propGetThemeVar('FieldLabelColor', propGetFormContext()?.fieldProps.value?.labelColor ?? 'text'),
- labelDisableColor: () => propGetThemeVar('FieldLabelDisableColor', propGetFormContext()?.fieldProps.value?.labelDisableColor ?? 'grey'),
- labelFlex: () => propGetThemeVar('FieldLabelFlex', propGetFormContext()?.labelFlex.value)!,
- inputDisableColor: () => propGetThemeVar('FieldInputDisableColor', propGetFormContext()?.fieldProps.value?.inputDisableColor ?? 'grey'),
- inputColor: () => propGetThemeVar('FieldInputColor', propGetFormContext()?.fieldProps.value?.inputColor ?? 'text'),
- inputFlex: () => propGetThemeVar('FieldInputFlex', propGetFormContext()?.inputFlex.value ?? 5),
- placeholderTextColor: () => propGetThemeVar('FieldPlaceholderTextColor', propGetFormContext()?.fieldProps.value?.placeholderTextColor ?? 'text.second'),
- errorTextColor: () => propGetThemeVar('FieldErrorTextColor', propGetFormContext()?.fieldProps.value?.errorTextColor ?? 'danger'),
- resetErrorOnClick: () => propGetThemeVar('FieldResetErrorOnClick', propGetFormContext()?.fieldProps.value?.resetErrorOnClick ?? true),
- colon: () => propGetFormContext()?.colon.value ?? true,
- fieldStyle: () => propGetFormContext()?.fieldProps.value?.fieldStyle ?? propGetThemeVar('FieldFieldStyle', {}),
- activeFieldStyle: () => propGetFormContext()?.fieldProps.value?.activeFieldStyle ?? propGetThemeVar('FieldActiveFieldStyle', {}),
- errorFieldStyle: () => propGetFormContext()?.fieldProps.value?.errorFieldStyle ?? propGetThemeVar('FieldErrorFieldStyle', {}),
- labelStyle: () => propGetThemeVar('FieldLabelStyle', propGetFormContext()?.fieldProps.value?.labelStyle ?? {}),
- inputStyle: () => propGetThemeVar('FieldInputStyle', propGetFormContext()?.fieldProps.value?.inputStyle ?? {}),
- activeInputStyle: () => propGetThemeVar('FieldActiveInputStyle', propGetFormContext()?.fieldProps.value?.activeInputStyle ?? {}),
- required: false,
- center: () => propGetFormContext()?.fieldProps.value?.center ?? true,
- showWordLimit: () => propGetFormContext()?.fieldProps.value?.showWordLimit ?? false,
- clearButton: () => propGetFormContext()?.fieldProps.value?.clearButton ?? false,
- clearButtonMode: () => propGetFormContext()?.fieldProps.value?.clearButtonMode ?? 'always',
- labelWidth: () => propGetFormContext()?.labelWidth.value ?? "left",
- labelAlign: () => propGetFormContext()?.labelAlign.value ?? "left",
- labelPosition: () => propGetFormContext()?.labelPosition.value ?? 'left',
- inputAlign: () => propGetFormContext()?.fieldProps.value?.inputAlign ?? "left",
- type: () => propGetFormContext()?.fieldProps.value?.type ?? "text",
- formatTrigger: () => propGetFormContext()?.fieldProps.value?.formatTrigger ?? 'input',
- showLabel: () => propGetFormContext()?.showLabel.value ?? true,
- showRequiredBadge: () => propGetFormContext()?.fieldProps.value?.showRequiredBadge ?? true,
- showRightArrow: () => propGetFormContext()?.fieldProps.value?.showRightArrow ?? false,
- disabled: false,
- readonly: false,
- autoHeight: true,
- maxLength: 100,
- modelValue: undefined,
- errorIcon: () => propGetThemeVar('FieldErrorIcon', 'prompt'),
- errorIconProps: () => propGetThemeVar('FieldErrorIconProps', propGetFormContext()?.fieldProps.value?.errorIconProps ?? {}),
- });
- //#region Context
- const formContextProps = inject<FormContext>('formContext', null as any);
- const error = ref<string|null>(null);
- let childRef : any = null;
- const childOnClickListener = ref<(() => void)|undefined>(undefined);
- //Context for parent
- const formItemInternalContext : FormItemInternalContext = {
- getValidateTrigger: () => props.validateTrigger || formContextProps?.validateTrigger.value || 'submit',
- getFieldName: () => props.name ?? '',
- setErrorState(errorMessage) { error.value = errorMessage; },
- getUniqueId() {
- return uniqueId;
- },
- setBlurState() {
- inputRef.value?.blur();
- childRef.value?.blur();
- },
- };
- //Context for custom children
- const formItemContext : FormItemContext = {
- getFieldName: (ref: any) => {
- if (ref)
- childRef = ref;
- return props.name ?? uniqueId;
- },
- onFieldFocus: () => {
- formContextProps?.onFieldFocus(formItemInternalContext);
- emit('focus');
- },
- onFieldBlur: () => {
- formContextProps?.onFieldBlur(formItemInternalContext);
- emit('blur');
- },
- getFormModelValue: () => formContextProps?.getItemValue(formItemInternalContext),
- onFieldChange: (newValue: unknown) => { formContextProps?.onFieldChange(formItemInternalContext, newValue); },
- clearValidate: () => { formContextProps?.clearValidate(formItemInternalContext); },
- setOnClickListener(listener: (() => void)|undefined) {
- childOnClickListener.value = listener;
- },
- }
- provide(FormItemContextContextKey, formItemContext);
- //Add ref in form
- const addNumber = formContextProps?.addFormItemField(formItemInternalContext);
- const uniqueId = (formContextProps?.name || 'form') + 'Item' + (props.name || `unknowProperity${addNumber}`);
- onBeforeUnmount(() => {
- formContextProps?.removeFormItemField(formItemInternalContext);
- })
- //#endregion
- const themeContext = useTheme();
- const themeStyles = themeContext.useThemeStyles({
- field: {
- backgroundColor: DynamicColor('FieldBackgroundColor', 'background.cell'),
- paddingVertical: DynamicSize('FieldPaddingVertical', 16),
- paddingHorizontal: DynamicSize('FieldPaddingHorizontal', 20),
- borderBottomWidth: DynamicSize('FieldBorderBottomWidth', '1px'),
- borderBottomColor: DynamicColor('FieldBorderBottomColor', 'border.cell'),
- borderBottomStyle: 'solid',
- },
- fieldVertical: {
- gap: DynamicSize('FieldVerticalGap', 20),
- },
- requiredMark: {
- fontSize: DynamicSize('FieldRequiredMark', 28),
- alignSelf: 'flex-start',
- color: DynamicColor('FieldRequiredMark', 'danger'),
- paddingVertical: DynamicSize('FieldRequiredMarkPaddingVertical', 8),
- marginHorizontal: DynamicSize('FieldRequiredMarkMarginHorizontal', 8),
- },
- labelText: {
- marginRight: DynamicSize('FieldLabelMarginRight', 20),
- fontSize: DynamicSize('FieldLabelFontSize', 28),
- alignSelf: 'flex-start',
- paddingVertical: DynamicSize('FieldLabelPaddingVertical', 8),
- },
- inputWapper2: {
- align: 'center',
- alignSelf: 'center',
- width: '100%',
- },
- input: {
- flex: 1,
- width: 'auto',
- minWidth: '100rpx',
- paddingVertical: DynamicSize('FieldInputPaddingVertical', 0),
- paddingHorizontal: DynamicSize('FieldInputPaddingHorizontal', 0),
- },
- errorMessageText: {
- fontSize: DynamicSize('FieldErrorMessageFontSize', 24),
- color: DynamicColor('FieldErrorMessageColor', 'danger'),
- },
- errorMessage: {
- marginTop: DynamicSize('FieldErrorMessageMarginTop', 12),
- },
- wordLimitText: {
- fontSize: DynamicSize('FieldWordLimitTextFontSize', 24),
- color: DynamicColor('FieldWordLimitTextColor', 'text.second'),
- width: DynamicSize('FieldWordLimitTextWidth', '100%'),
- textAlign: DynamicVar('FieldWordLimitTextTextAlign', 'right'),
- },
- clearIcon: {
- width: DynamicSize('FieldClearIconWidth', 60),
- justifyContent: 'center',
- alignItems: 'center',
- },
- });
- const requiredShow = computed(() => {
- return props.required == true || formContextProps?.getItemRequieed(formItemInternalContext);
- });
- const wordLimitText = computed(() => {
- let wordString = props.modelValue ? props.modelValue.length : '0';
- if (props.maxLength)
- wordString += `/${props.maxLength}`;
- else
- wordString += `字`;
- return wordString
- })
- const finalErrorMessage = computed(() => {
- return props.errorMessage || error.value || '';
- })
- const inputValue = ref();
- const focused = ref(false);
- const inputRef = ref();
- watch(() => props.modelValue, (newValue) => {
- inputValue.value = newValue;
- });
- onMounted(() => {
- inputValue.value = props.modelValue ?? formItemContext.getFormModelValue();
- });
- function emitChangeText(text: string) {
- emit('update:modelValue', text);
- inputValue.value = text;
- formItemContext.onFieldChange(text);
- }
- function doFormatter(text: string) {
- switch (props.type) {
- case 'decimal':
- text = text.replace(/[^\d.]/g, "");
- text = text.replace(/^\./g, ""); //必须保证第一个为数字而不是.
- text = text.replace(/\.{2,}/g, "."); //保证只有出现一个.而没有多个.
- text = text.replace(".","$#$").replace(/\./g, "").replace("$#$", ".");
- break;
- case 'number':
- text = text.replace(/[^\d]/g, '');
- break;
- case 'tel':
- text = text.replace(/[^(\d|\-|*|#)]/g, '');
- break;
- }
- if (props.formatter)
- text = props.formatter(text);
- return text;
- }
- function onInput(e: any) {
- focused.value = true;
- let text = e.detail.value;
- if (props.formatTrigger !== 'blur') //格式化字符串
- text = doFormatter(text);
- emitChangeText(text);
- }
- function onFocus() {
- focused.value = true;
- formItemContext.onFieldFocus();
- }
- function onBlur() {
- focused.value = false;
- if (props.formatTrigger === 'blur'){//格式化字符串
- const text = doFormatter(props.modelValue ?? '');
- emitChangeText(text);
- }
- formItemContext.onFieldBlur();
- }
- function onClear() {
- //清空按钮
- emitChangeText('');
- emit('clear');
- }
- function onClick() {
- childOnClickListener.value?.();
- emit('click');
- if (props.resetErrorOnClick)
- fieldInstance.clearValidate();
- }
- const fieldInstance : FieldInstance = {
- focus() {
- inputRef.value?.focus();
- childRef.value?.focus();
- },
- blur() {
- inputRef.value?.blur();
- childRef.value?.blur();
- },
- clear() {
- inputRef.value?.clear();
- childRef.value?.clear();
- },
- isFocused() :boolean {
- return focused.value;
- },
- clearValidate() {
- formContextProps?.clearValidate(formItemInternalContext);
- }
- }
- defineExpose<FieldInstance>(fieldInstance);
- </script>
|