Field.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. <template>
  2. <Touchable
  3. :touchable="touchable || childOnClickListener !== undefined"
  4. :pressedColor="themeContext.resolveThemeColor('FieldPressedColor', 'pressed.white')"
  5. :innerStyle="{
  6. ...themeStyles.field.value,
  7. ...(showBottomBorder ? themeStyles.fieldBorder.value : {}),
  8. ...(labelPosition === 'top' ? themeStyles.fieldVertical.value : {}),
  9. ...fieldStyle,
  10. ...(focused ? activeFieldStyle : {}),
  11. ...(error || finalErrorMessage ? errorFieldStyle : {})
  12. }"
  13. :direction="labelPosition === 'top' ? 'column' : 'row'"
  14. :justify="labelPosition === 'top' ? 'flex-start' : 'center'"
  15. @click="onClick"
  16. >
  17. <!-- 左边的标签区域 -->
  18. <FlexRow
  19. v-if="showLabel !== false && label"
  20. align="center"
  21. :flex="labelFlex"
  22. :innerStyle="{
  23. width: themeContext.resolveThemeSize(labelWidth),
  24. }"
  25. :justify="selectStyleType(labelAlign, 'left', {
  26. left: 'flex-start',
  27. center: 'center',
  28. right: 'flex-end',
  29. })"
  30. >
  31. <text v-if="requiredShow && showRequiredBadge" :style="themeStyles.requiredMark.value">*</text>
  32. <slot name="leftIcon" />
  33. <view v-if="!label" :style="labelStyle" />
  34. <text v-else :style="{
  35. ...themeStyles.labelText.value,
  36. ...labelStyle,
  37. color: themeContext.resolveThemeColor(disabled ? labelDisableColor : labelColor),
  38. }">
  39. {{ label + (colon ? ': ' : '') }}
  40. </text>
  41. </FlexRow>
  42. <!-- 输入框区域 -->
  43. <FlexCol :flex="inputFlex">
  44. <FlexRow
  45. justify="space-between"
  46. align="center"
  47. :innerStyle="themeStyles.inputWapper2.value"
  48. >
  49. <slot name="prefix" />
  50. <slot name="leftButton" />
  51. <slot name="control" />
  52. <slot>
  53. <FlexRow v-if="tags" wrap align="center" :gap="10">
  54. <Tag
  55. v-for="(tag, index) in tagSplited"
  56. :key="index"
  57. :text="tag"
  58. color="primary"
  59. size="small"
  60. closeable
  61. @close="onTagDelete(tag)"
  62. />
  63. <input
  64. ref="inputRef"
  65. :style="{
  66. ...themeStyles.input.value,
  67. ...inputStyle,
  68. ...(focused ? activeInputStyle : {}),
  69. color: themeContext.resolveThemeColor(disabled ? inputDisableColor : (error ? errorTextColor : inputColor)),
  70. }"
  71. :value="tagInputString"
  72. :placeholder="placeholder"
  73. :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
  74. confirm-type="done"
  75. type="text"
  76. :maxlength="200"
  77. :disabled="disabled"
  78. :readonly="readonly"
  79. @focus="onFocus"
  80. @blur="onBlur"
  81. @input="onTagInputInput"
  82. @confirm="onTagInputConfirm"
  83. />
  84. </FlexRow>
  85. <textarea
  86. v-else-if="multiline"
  87. ref="inputRef"
  88. :style="{
  89. ...themeStyles.input.value,
  90. ...inputStyle,
  91. ...(focused ? activeInputStyle : {}),
  92. color: themeContext.resolveThemeColor(disabled ? inputDisableColor : (error ? errorTextColor : inputColor)),
  93. textAlign: inputAlign,
  94. }"
  95. :autoHeight="autoHeight"
  96. :value="inputValue"
  97. :password="type==='password'"
  98. :placeholder="placeholder"
  99. :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
  100. confirm-type="done"
  101. :maxlength="maxLength"
  102. :disabled="disabled"
  103. :readonly="readonly"
  104. @input="onInput"
  105. @focus="onFocus"
  106. @blur="onBlur"
  107. @confirm="onBlur"
  108. @click="onClick"
  109. />
  110. <input
  111. v-else
  112. ref="inputRef"
  113. :style="{
  114. ...themeStyles.input.value,
  115. ...inputStyle,
  116. ...(focused ? activeInputStyle : {}),
  117. color: themeContext.resolveThemeColor(disabled ? inputDisableColor : (error ? errorTextColor : inputColor)),
  118. textAlign: inputAlign,
  119. }"
  120. :value="inputValue"
  121. :password="type==='password'"
  122. :placeholder="placeholder"
  123. :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
  124. confirm-type="done"
  125. :type="selectStyleType(type, 'text', {
  126. text: 'text',
  127. password: 'text',
  128. number: 'number',
  129. decimal: 'digit',
  130. tel: 'tel',
  131. email: 'text',
  132. }) || 'text'"
  133. :maxlength="maxLength"
  134. :disabled="disabled"
  135. :readonly="readonly"
  136. @input="onInput"
  137. @focus="onFocus"
  138. @blur="onBlur"
  139. @confirm="onBlur"
  140. @click="onClick"
  141. />
  142. </slot>
  143. <slot name="suffix" />
  144. <slot name="rightButton" />
  145. <Icon v-if="showRightArrow" icon="arrow-right" :size="themeContext.resolveThemeSize('FieldRightArrowSize', 30)" v-bind="rightArrowProps" />
  146. </FlexRow>
  147. <!-- 错误提示图标和文本 -->
  148. <FlexRow
  149. v-if="finalErrorMessage"
  150. :gap="10"
  151. :innerStyle="themeStyles.errorMessage.value"
  152. align="center"
  153. >
  154. <Icon
  155. :icon="errorIcon"
  156. :size="themeContext.resolveThemeSize('FieldErrorIconSize', 40)"
  157. v-bind="errorIconProps"
  158. :color="themeContext.resolveThemeColor('FieldErrorMessageColor', 'danger')"
  159. />
  160. <text :style="themeStyles.errorMessageText.value">{{finalErrorMessage}}</text>
  161. </FlexRow>
  162. <!-- 额外的提示信息 -->
  163. <FlexRow
  164. v-if="extraMessage && !readonly"
  165. :gap="10"
  166. :innerStyle="themeStyles.extraMessage.value"
  167. align="center"
  168. >
  169. <Icon
  170. :icon="extraIcon"
  171. :size="themeContext.resolveThemeSize('FieldExtraIconSize', 40)"
  172. v-bind="extraIconProps"
  173. :color="themeContext.resolveThemeColor('FieldExtraIconColor', 'text.second')"
  174. />
  175. <text :style="themeStyles.extraMessageText.value">{{extraMessage}}</text>
  176. </FlexRow>
  177. <text v-if="showWordLimit" :style="themeStyles.wordLimitText.value">{{wordLimitText}}</text>
  178. </FlexCol>
  179. <!-- 清除按钮 -->
  180. <IconButton
  181. v-if="(clearButton && (
  182. clearButtonMode === 'always'
  183. || (clearButtonMode === 'while-editing' && focused))
  184. || (clearButtonMode === 'unless-editing' && modelValue !== '')
  185. )"
  186. icon="delete-filling"
  187. :color="themeContext.resolveThemeColor('FieldClearIconColor', 'grey')"
  188. v-bind="clearButtonProps"
  189. @click="onClear"
  190. />
  191. </Touchable>
  192. </template>
  193. <script setup lang="ts">
  194. import { computed, inject, onBeforeUnmount, onMounted, provide, ref, toRef, watch } from 'vue';
  195. import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
  196. import { FormItemContextContextKey, propGetFormContext, type FormContext, type FormItemContext, type FormItemInternalContext } from './FormContext';
  197. import { DynamicColor, DynamicSize, DynamicVar, selectStyleType } from '../theme/ThemeTools';
  198. import type { IconProps } from '../basic/Icon.vue';
  199. import Icon from '../basic/Icon.vue';
  200. import IconButton from '../basic/IconButton.vue';
  201. import FlexCol from '../layout/FlexCol.vue';
  202. import FlexRow from '../layout/FlexRow.vue';
  203. import Touchable from '../feedback/Touchable.vue';
  204. import type { TextProps } from '../basic/Text.vue';
  205. import type { RuleItem } from 'async-validator';
  206. import Tag from '../display/Tag.vue';
  207. export interface FieldInstance {
  208. /**
  209. * 获取输入框焦点
  210. */
  211. focus: () => void;
  212. /**
  213. * 取消输入框焦点
  214. */
  215. blur: () => void;
  216. /**
  217. * 清空输入框
  218. */
  219. clear: () => void;
  220. /**
  221. * 返回值表明当前输入框是否获得了焦点。
  222. */
  223. isFocused: () => boolean;
  224. /**
  225. * 清除当前条目的校验状态
  226. */
  227. clearValidate: () => void;
  228. }
  229. export interface FieldProps {
  230. modelValue?: any,
  231. /**
  232. * 输入框左侧文本
  233. */
  234. label?: string;
  235. /**
  236. * 名称,作为提交表单时的标识符
  237. */
  238. name?: string;
  239. /**
  240. * 是否可点击.可以作为一个纯点击条目
  241. */
  242. touchable?: boolean;
  243. /**
  244. * 输入框类型
  245. * @default 'text'
  246. */
  247. type?: 'text'|'tel'|'number'|'password'|'number'|'email'|'decimal';
  248. /**
  249. * 输入的最大字符数
  250. */
  251. maxLength?: number;
  252. /**
  253. * 输入框占位提示文字
  254. */
  255. placeholder?: string;
  256. /**
  257. * 是否禁用输入框
  258. * @default false
  259. */
  260. disabled?: boolean;
  261. /**
  262. * 是否只读
  263. * @default false
  264. */
  265. readonly?: boolean;
  266. /**
  267. * 是否内容垂直居中
  268. * @default false
  269. */
  270. center?: boolean;
  271. /**
  272. * 是否在 label 后面添加冒号
  273. * @default true
  274. */
  275. colon?: boolean;
  276. /**
  277. * 是否必填
  278. * @default false
  279. */
  280. required?: boolean;
  281. /**
  282. * 多行文字
  283. * @default false
  284. */
  285. multiline?: boolean;
  286. /**
  287. * 多行文字下是否自动调整高度
  288. * @default false
  289. */
  290. autoHeight?: boolean;
  291. /**
  292. * 是否显示表单必填星号
  293. * @default true
  294. */
  295. showRequiredBadge?: boolean;
  296. /**
  297. * 是否启用清除图标,点击清除图标后会清空输入框
  298. * @default false
  299. */
  300. clearButton?: boolean;
  301. /**
  302. * 清除图标的自定义属性
  303. */
  304. clearButtonProps?: IconProps;
  305. /**
  306. * 清除图标的显示模式
  307. */
  308. clearButtonMode?: 'always'|'while-editing'|'unless-editing';
  309. /**
  310. * 左侧文本的宽度
  311. */
  312. labelWidth?: string|number|undefined;
  313. /**
  314. * 左侧文本对齐
  315. * @default 'left'
  316. */
  317. labelAlign?: 'left'|'center'|'right';
  318. /**
  319. * 左侧文本的位置
  320. * @default 'left'
  321. */
  322. labelPosition?: 'top'|'left';
  323. /**
  324. * 左侧文本的flex占比
  325. * @default undefined
  326. */
  327. labelFlex?: number;
  328. /**
  329. * 输入框的flex占比
  330. * @default 5
  331. */
  332. inputFlex?: number;
  333. /**
  334. * 左侧文本的样式
  335. */
  336. labelStyle?: TextStyle;
  337. /**
  338. * 左侧文本的颜色
  339. */
  340. labelColor?: string;
  341. /**
  342. * 左侧文本的禁用颜色
  343. */
  344. labelDisableColor?: string;
  345. /**
  346. * 输入框样式
  347. */
  348. inputStyle?: TextStyle;
  349. /**
  350. * 激活时的外壳样式
  351. */
  352. activeInputStyle?: TextStyle;
  353. /**
  354. * 输入框颜色
  355. */
  356. inputColor?: string;
  357. /**
  358. * 输入框文本对齐。
  359. * @default 'left'
  360. */
  361. inputAlign?: 'left'|'center'|'right';
  362. /**
  363. * 输入框禁用颜色
  364. */
  365. inputDisableColor?: string;
  366. /**
  367. * 外壳样式
  368. */
  369. fieldStyle?: ViewStyle;
  370. /**
  371. * 激活时的外壳样式
  372. */
  373. activeFieldStyle?: ViewStyle;
  374. /**
  375. * 错误时的外壳样式
  376. */
  377. errorFieldStyle?: ViewStyle;
  378. /**
  379. * 错误时的文字颜色
  380. * @default Color.danger
  381. */
  382. errorTextColor?: string;
  383. /**
  384. * 额外的提示信息
  385. */
  386. extraMessage?: string;
  387. extraMessageProps?: TextProps;
  388. /**
  389. * 额外的提示图标
  390. * @default 'prompt'
  391. */
  392. extraIcon?: string;
  393. extraIconProps?: IconProps;
  394. /**
  395. * 文本框水印文字颜色
  396. * @default Color.grey
  397. */
  398. placeholderTextColor?: string;
  399. /**
  400. * 输入内容格式化函数
  401. */
  402. formatter?: (text: string) => string;
  403. /**
  404. * 格式化函数触发的时机,可选值为 blur
  405. * @default 'input'
  406. */
  407. formatTrigger?: 'blur'|'input';
  408. /**
  409. * 设置字段校验的时机
  410. * * blur 文本框失去焦点时校验
  411. * * change 数值更改时校验
  412. * * submit 提交时校验(默认)
  413. * @default 'submit'
  414. */
  415. validateTrigger?: 'blur'|'change'|'submit';
  416. /**
  417. * 是否将输入内容标红。
  418. */
  419. error?: boolean;
  420. /**
  421. * 错误提示的图标
  422. * @default 'prompt'
  423. */
  424. errorIcon?: string;
  425. /**
  426. * 错误提示图标的自定义属性
  427. */
  428. errorIconProps?: IconProps;
  429. /**
  430. * 底部错误提示文案,为空时不展示
  431. */
  432. errorMessage?: string;
  433. /**
  434. * 点击输入框是否重置错误状态
  435. * @default true
  436. */
  437. resetErrorOnClick?: boolean;
  438. /**
  439. * 是否显示字数统计
  440. * @default false
  441. */
  442. showWordLimit?: boolean;
  443. /**
  444. * 是否显左边标题
  445. * @default true
  446. */
  447. showLabel?: boolean;
  448. /**
  449. * 是否显右边箭头
  450. * @default false
  451. */
  452. showRightArrow?: boolean;
  453. /**
  454. * 是否显示底部边框
  455. * @default true
  456. */
  457. showBottomBorder?: boolean;
  458. /**
  459. * 右边箭头的自定义属性
  460. */
  461. rightArrowProps?: IconProps;
  462. /**
  463. * 当前条目的校验规则
  464. */
  465. rules?: RuleItem[],
  466. /**
  467. * 是否显示输入标签模式
  468. * @default false
  469. */
  470. tags?: boolean,
  471. /**
  472. * 输入标签模式下,标签之间的连接符
  473. * @default ','
  474. */
  475. tagJoinType?: string,
  476. requireChildRef?: () => any,
  477. }
  478. defineOptions({
  479. options: {
  480. styleIsolation: "shared",
  481. virtualHost: true,
  482. }
  483. })
  484. const emit = defineEmits([ 'update:modelValue', 'click', 'blur', 'focus', 'clear' ])
  485. const props = withDefaults(defineProps<FieldProps>(), {
  486. label: '',
  487. labelColor: () => propGetThemeVar('FieldLabelColor', propGetFormContext()?.fieldProps.value?.labelColor ?? 'text'),
  488. labelDisableColor: () => propGetThemeVar('FieldLabelDisableColor', propGetFormContext()?.fieldProps.value?.labelDisableColor ?? 'grey'),
  489. labelFlex: () => propGetThemeVar('FieldLabelFlex', propGetFormContext()?.labelFlex.value)!,
  490. inputDisableColor: () => propGetThemeVar('FieldInputDisableColor', propGetFormContext()?.fieldProps.value?.inputDisableColor ?? 'grey'),
  491. inputColor: () => propGetThemeVar('FieldInputColor', propGetFormContext()?.fieldProps.value?.inputColor ?? 'text'),
  492. inputFlex: () => propGetThemeVar('FieldInputFlex', propGetFormContext()?.inputFlex.value ?? 5),
  493. placeholderTextColor: () => propGetThemeVar('FieldPlaceholderTextColor', propGetFormContext()?.fieldProps.value?.placeholderTextColor ?? 'text.second'),
  494. errorTextColor: () => propGetThemeVar('FieldErrorTextColor', propGetFormContext()?.fieldProps.value?.errorTextColor ?? 'danger'),
  495. resetErrorOnClick: () => propGetThemeVar('FieldResetErrorOnClick', propGetFormContext()?.fieldProps.value?.resetErrorOnClick ?? true),
  496. colon: () => propGetFormContext()?.colon.value ?? true,
  497. fieldStyle: () => propGetFormContext()?.fieldProps.value?.fieldStyle ?? propGetThemeVar('FieldFieldStyle', {}),
  498. activeFieldStyle: () => propGetFormContext()?.fieldProps.value?.activeFieldStyle ?? propGetThemeVar('FieldActiveFieldStyle', {}),
  499. errorFieldStyle: () => propGetFormContext()?.fieldProps.value?.errorFieldStyle ?? propGetThemeVar('FieldErrorFieldStyle', {}),
  500. labelStyle: () => propGetThemeVar('FieldLabelStyle', propGetFormContext()?.fieldProps.value?.labelStyle ?? {}),
  501. inputStyle: () => propGetThemeVar('FieldInputStyle', propGetFormContext()?.fieldProps.value?.inputStyle ?? {}),
  502. activeInputStyle: () => propGetThemeVar('FieldActiveInputStyle', propGetFormContext()?.fieldProps.value?.activeInputStyle ?? {}),
  503. required: false,
  504. showBottomBorder: () => propGetFormContext()?.fieldProps.value?.showBottomBorder ?? true,
  505. center: () => propGetFormContext()?.fieldProps.value?.center ?? true,
  506. showWordLimit: () => propGetFormContext()?.fieldProps.value?.showWordLimit ?? false,
  507. clearButton: () => propGetFormContext()?.fieldProps.value?.clearButton ?? false,
  508. clearButtonMode: () => propGetFormContext()?.fieldProps.value?.clearButtonMode ?? 'always',
  509. labelWidth: () => propGetFormContext()?.labelWidth.value!,
  510. labelAlign: () => propGetFormContext()?.labelAlign.value ?? "left",
  511. labelPosition: () => propGetFormContext()?.labelPosition.value ?? 'left',
  512. inputAlign: () => propGetFormContext()?.fieldProps.value?.inputAlign ?? "left",
  513. type: () => propGetFormContext()?.fieldProps.value?.type ?? "text",
  514. formatTrigger: () => propGetFormContext()?.fieldProps.value?.formatTrigger ?? 'input',
  515. showLabel: () => propGetFormContext()?.showLabel.value ?? true,
  516. showRequiredBadge: () => propGetFormContext()?.fieldProps.value?.showRequiredBadge ?? true,
  517. showRightArrow: () => propGetFormContext()?.fieldProps.value?.showRightArrow ?? false,
  518. disabled: () => propGetFormContext()?.disabled.value ?? false,
  519. readonly: () => propGetFormContext()?.readonly.value ?? false,
  520. autoHeight: false,
  521. maxLength: 100,
  522. modelValue: undefined,
  523. tags: false,
  524. tagJoinType: ',',
  525. errorIcon: () => propGetThemeVar('FieldErrorIcon', 'prompt'),
  526. errorIconProps: () => propGetThemeVar('FieldErrorIconProps', propGetFormContext()?.fieldProps.value?.errorIconProps ?? {}),
  527. extraMessage: '',
  528. extraMessageProps: () => propGetThemeVar('FieldExtraMessageProps', propGetFormContext()?.fieldProps.value?.extraMessageProps ?? {}),
  529. extraIcon: () => propGetThemeVar('FieldExtraIcon', 'prompt'),
  530. extraIconProps: () => propGetThemeVar('FieldExtraIconProps', propGetFormContext()?.fieldProps.value?.extraIconProps ?? {}),
  531. });
  532. //#region Context
  533. const formContextProps = inject<FormContext>('formContext', null as any);
  534. const error = ref<string|null>(null);
  535. const finalErrorMessage = computed(() => {
  536. return props.errorMessage || error.value || '';
  537. })
  538. const errorState = computed(() => Boolean(finalErrorMessage.value));
  539. const childOnClickListener = ref<(() => void)|undefined>(undefined);
  540. //Context for parent
  541. const formItemInternalContext : FormItemInternalContext = {
  542. getExpectedRef: () => {
  543. return props.requireChildRef?.() ?? fieldInstance;
  544. },
  545. getItemRules: () => props.rules ?? [],
  546. getValidateTrigger: () => props.validateTrigger || formContextProps?.validateTrigger.value || 'submit',
  547. getFieldName: () => props.name ?? '',
  548. setErrorState(errorMessage) { error.value = errorMessage; },
  549. getUniqueId() {
  550. return uniqueId;
  551. },
  552. setBlurState() {
  553. inputRef.value?.blur();
  554. },
  555. };
  556. //Context for custom children
  557. const formItemContext : FormItemContext = {
  558. errorState: errorState,
  559. getFieldName: () => {
  560. return props.name ?? uniqueId;
  561. },
  562. onFieldFocus: () => {
  563. formContextProps?.onFieldFocus(formItemInternalContext);
  564. emit('focus');
  565. },
  566. onFieldBlur: () => {
  567. formContextProps?.onFieldBlur(formItemInternalContext);
  568. emit('blur');
  569. },
  570. getFormModelValue: () => formContextProps?.getItemValue(formItemInternalContext),
  571. onFieldChange: (newValue: unknown) => { formContextProps?.onFieldChange(formItemInternalContext, newValue); },
  572. clearValidate: () => { formContextProps?.clearValidate(formItemInternalContext); },
  573. setOnClickListener(listener: (() => void)|undefined) {
  574. childOnClickListener.value = listener;
  575. },
  576. disabled: toRef(props, 'disabled'),
  577. readonly: toRef(props, 'readonly'),
  578. }
  579. provide(FormItemContextContextKey, formItemContext);
  580. //Add ref in form
  581. const addNumber = formContextProps?.addFormItemField(formItemInternalContext);
  582. const uniqueId = (formContextProps?.name || 'form') + 'Item' + (props.name || `unknowProperity${addNumber}`);
  583. onBeforeUnmount(() => {
  584. formContextProps?.removeFormItemField(formItemInternalContext);
  585. })
  586. //#endregion
  587. const themeContext = useTheme();
  588. const themeStyles = themeContext.useThemeStyles({
  589. field: {
  590. backgroundColor: DynamicColor('FieldBackgroundColor', 'background.cell'),
  591. paddingVertical: DynamicSize('FieldPaddingVertical', 16),
  592. paddingHorizontal: DynamicSize('FieldPaddingHorizontal', 20),
  593. },
  594. fieldBorder: {
  595. borderBottomWidth: DynamicSize('FieldBorderBottomWidth', '1px'),
  596. borderBottomColor: DynamicColor('FieldBorderBottomColor', 'border.cell'),
  597. borderBottomStyle: 'solid',
  598. },
  599. fieldVertical: {
  600. gap: DynamicSize('FieldVerticalGap', 20),
  601. },
  602. requiredMark: {
  603. fontSize: DynamicSize('FieldRequiredMark', 28),
  604. alignSelf: 'flex-start',
  605. color: DynamicColor('FieldRequiredMark', 'danger'),
  606. paddingVertical: DynamicSize('FieldRequiredMarkPaddingVertical', 8),
  607. marginHorizontal: DynamicSize('FieldRequiredMarkMarginHorizontal', 8),
  608. },
  609. labelText: {
  610. marginRight: DynamicSize('FieldLabelMarginRight', 20),
  611. fontSize: DynamicSize('FieldLabelFontSize', 28),
  612. alignSelf: 'flex-start',
  613. paddingVertical: DynamicSize('FieldLabelPaddingVertical', 8),
  614. },
  615. inputWapper2: {
  616. align: 'center',
  617. alignSelf: 'center',
  618. width: '100%',
  619. },
  620. input: {
  621. flex: 1,
  622. width: 'auto',
  623. minWidth: '100rpx',
  624. paddingVertical: DynamicSize('FieldInputPaddingVertical', 0),
  625. paddingHorizontal: DynamicSize('FieldInputPaddingHorizontal', 0),
  626. },
  627. errorMessageText: {
  628. fontSize: DynamicSize('FieldErrorMessageFontSize', 24),
  629. color: DynamicColor('FieldErrorMessageColor', 'danger'),
  630. },
  631. errorMessage: {
  632. marginTop: DynamicSize('FieldErrorMessageMarginTop', 12),
  633. },
  634. extraMessageText: {
  635. fontSize: DynamicSize('FieldExtraMessageFontSize', 24),
  636. color: DynamicColor('FieldExtraMessageColor', 'text.second'),
  637. },
  638. extraMessage: {
  639. marginTop: DynamicSize('FieldExtraMessageMarginTop', 12),
  640. },
  641. wordLimitText: {
  642. fontSize: DynamicSize('FieldWordLimitTextFontSize', 24),
  643. color: DynamicColor('FieldWordLimitTextColor', 'text.second'),
  644. width: DynamicSize('FieldWordLimitTextWidth', '100%'),
  645. textAlign: DynamicVar('FieldWordLimitTextTextAlign', 'right'),
  646. },
  647. clearIcon: {
  648. width: DynamicSize('FieldClearIconWidth', 60),
  649. justifyContent: 'center',
  650. alignItems: 'center',
  651. },
  652. });
  653. const requiredShow = computed(() => {
  654. return props.required == true || formContextProps?.getItemRequieed(formItemInternalContext);
  655. });
  656. const wordLimitText = computed(() => {
  657. let wordString = props.modelValue ? props.modelValue.length : '0';
  658. if (props.maxLength)
  659. wordString += `/${props.maxLength}`;
  660. else
  661. wordString += `字`;
  662. return wordString
  663. });
  664. const inputValue = ref();
  665. const focused = ref(false);
  666. const inputRef = ref();
  667. watch(() => props.modelValue, (newValue) => {
  668. inputValue.value = newValue;
  669. });
  670. onMounted(() => {
  671. inputValue.value = props.modelValue ?? formItemContext.getFormModelValue();
  672. });
  673. function emitChangeText(text: string) {
  674. emit('update:modelValue', text);
  675. inputValue.value = text;
  676. formItemContext.onFieldChange(text);
  677. }
  678. function doFormatter(text: string) {
  679. switch (props.type) {
  680. case 'decimal':
  681. text = text.replace(/[^\d.]/g, "");
  682. text = text.replace(/^\./g, ""); //必须保证第一个为数字而不是.
  683. text = text.replace(/\.{2,}/g, "."); //保证只有出现一个.而没有多个.
  684. text = text.replace(".","$#$").replace(/\./g, "").replace("$#$", ".");
  685. break;
  686. case 'number':
  687. text = text.replace(/[^\d]/g, '');
  688. break;
  689. case 'tel':
  690. text = text.replace(/[^(\d|\-|*|#)]/g, '');
  691. break;
  692. }
  693. if (props.formatter)
  694. text = props.formatter(text);
  695. return text;
  696. }
  697. function onInput(e: any) {
  698. focused.value = true;
  699. let text = e.detail.value;
  700. if (props.formatTrigger !== 'blur') //格式化字符串
  701. text = doFormatter(text);
  702. emitChangeText(text);
  703. }
  704. function onFocus() {
  705. focused.value = true;
  706. formItemContext.onFieldFocus();
  707. }
  708. function onBlur() {
  709. focused.value = false;
  710. if (props.formatTrigger === 'blur'){//格式化字符串
  711. const text = doFormatter(props.modelValue ?? '');
  712. emitChangeText(text);
  713. }
  714. formItemContext.onFieldBlur();
  715. }
  716. function onClear() {
  717. //清空按钮
  718. emitChangeText('');
  719. emit('clear');
  720. }
  721. function onClick() {
  722. if (props.resetErrorOnClick)
  723. fieldInstance.clearValidate();
  724. if (props.disabled || props.readonly)
  725. return;
  726. childOnClickListener.value?.();
  727. emit('click');
  728. }
  729. //#region 标签输入框事件处理
  730. const tagInputString = ref('');
  731. const tagSplited = computed<string[]>(() => {
  732. let str = props.modelValue;
  733. if (typeof str === 'string')
  734. return (props.modelValue as string|| '').split(props.tagJoinType).filter(x => x);
  735. return [];
  736. });
  737. function onTagDelete(tag: string) {
  738. const tags = tagSplited.value.concat();
  739. const index = tags.indexOf(tag);
  740. if (index !== -1) {
  741. tags.splice(index, 1);
  742. emitChangeText(tags.join(props.tagJoinType));
  743. }
  744. }
  745. function onTagInputInput(e: any) {
  746. focused.value = true;
  747. tagInputString.value = e.detail.value;
  748. }
  749. function onTagInputConfirm() {
  750. if (tagInputString.value && !tagSplited.value.includes(tagInputString.value)) {
  751. emitChangeText(props.modelValue + props.tagJoinType + tagInputString.value);
  752. tagInputString.value = '';
  753. }
  754. }
  755. //#endregion
  756. const fieldInstance : FieldInstance = {
  757. focus() {
  758. inputRef.value?.focus();
  759. },
  760. blur() {
  761. inputRef.value?.blur();
  762. },
  763. clear() {
  764. inputRef.value?.clear();
  765. },
  766. isFocused() :boolean {
  767. return focused.value;
  768. },
  769. clearValidate() {
  770. formContextProps?.clearValidate(formItemInternalContext);
  771. }
  772. }
  773. defineExpose<FieldInstance>(fieldInstance);
  774. </script>