Field.vue 25 KB

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