cl-input.uvue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <template>
  2. <view
  3. class="cl-input"
  4. :class="[
  5. pt.className,
  6. {
  7. 'is-dark': isDark,
  8. 'cl-input--border': border,
  9. 'cl-input--focus': isFocus,
  10. 'cl-input--disabled': isDisabled,
  11. 'cl-input--error': isError
  12. }
  13. ]"
  14. @tap="onTap"
  15. >
  16. <slot name="prepend"></slot>
  17. <view class="cl-input__icon !pl-0 pr-[12rpx]" v-if="prefixIcon">
  18. <cl-icon
  19. :name="prefixIcon"
  20. :size="pt.prefixIcon?.size ?? 32"
  21. :pt="{ className: parseClass([pt.prefixIcon?.className]) }"
  22. ></cl-icon>
  23. </view>
  24. <input
  25. class="cl-input__inner"
  26. :class="[
  27. {
  28. 'is-disabled': isDisabled,
  29. 'is-dark': isDark
  30. },
  31. pt.inner?.className
  32. ]"
  33. :value="value"
  34. :disabled="readonly ?? isDisabled"
  35. :type="type"
  36. :password="isPassword"
  37. :focus="isFocus"
  38. :placeholder="placeholder"
  39. :placeholder-class="`text-surface-400 ${placeholderClass}`"
  40. :maxlength="maxlength"
  41. :cursor-spacing="cursorSpacing"
  42. :confirm-type="confirmType"
  43. :confirm-hold="confirmHold"
  44. :adjust-position="adjustPosition"
  45. :hold-keyboard="holdKeyboard"
  46. @input="onInput"
  47. @focus="onFocus"
  48. @blur="onBlur"
  49. @confirm="onConfirm"
  50. @keyboardheightchange="onKeyboardheightchange"
  51. />
  52. <view class="cl-input__icon" v-if="suffixIcon">
  53. <cl-icon
  54. :name="suffixIcon"
  55. :size="pt.suffixIcon?.size ?? 32"
  56. :pt="{ className: parseClass([pt.prefixIcon?.className]) }"
  57. ></cl-icon>
  58. </view>
  59. <view class="cl-input__icon" @tap="clear" v-if="showClear">
  60. <cl-icon
  61. name="close-circle-fill"
  62. :size="32"
  63. :pt="{ className: '!text-surface-400' }"
  64. ></cl-icon>
  65. </view>
  66. <view class="cl-input__icon" @tap="showPassword" v-if="password">
  67. <cl-icon
  68. :name="isPassword ? 'eye-line' : 'eye-off-line'"
  69. :size="32"
  70. :pt="{ className: '!text-surface-300' }"
  71. ></cl-icon>
  72. </view>
  73. <slot name="append"></slot>
  74. </view>
  75. </template>
  76. <script setup lang="ts">
  77. import { computed, nextTick, onMounted, ref, watch, type PropType } from "vue";
  78. import type { ClInputType, PassThroughProps } from "../../types";
  79. import { isDark, parseClass, parsePt } from "@/cool";
  80. import type { ClIconProps } from "../cl-icon/props";
  81. import { t } from "@/locale";
  82. import { useForm, useFormItem } from "../../hooks";
  83. defineOptions({
  84. name: "cl-input"
  85. });
  86. // 组件属性定义
  87. const props = defineProps({
  88. // 透传样式
  89. pt: {
  90. type: Object,
  91. default: () => ({})
  92. },
  93. // 绑定值
  94. modelValue: {
  95. type: String,
  96. default: ""
  97. },
  98. // 输入框类型
  99. type: {
  100. type: String as PropType<ClInputType>,
  101. default: "text"
  102. },
  103. // 前缀图标
  104. prefixIcon: {
  105. type: String,
  106. default: ""
  107. },
  108. // 后缀图标
  109. suffixIcon: {
  110. type: String,
  111. default: ""
  112. },
  113. // 是否密码框
  114. password: {
  115. type: Boolean,
  116. default: false
  117. },
  118. // 是否自动聚焦
  119. autofocus: {
  120. type: Boolean,
  121. default: false
  122. },
  123. // 是否禁用
  124. disabled: {
  125. type: Boolean,
  126. default: false
  127. },
  128. // 是否只读
  129. readonly: {
  130. type: Boolean,
  131. default: null
  132. },
  133. // 占位符
  134. placeholder: {
  135. type: String,
  136. default: () => t("请输入")
  137. },
  138. // 占位符样式类
  139. placeholderClass: {
  140. type: String,
  141. default: ""
  142. },
  143. // 是否显示边框
  144. border: {
  145. type: Boolean,
  146. default: true
  147. },
  148. // 是否可清除
  149. clearable: {
  150. type: Boolean,
  151. default: false
  152. },
  153. // 光标与键盘的距离
  154. cursorSpacing: {
  155. type: Number,
  156. default: 5
  157. },
  158. // 点击键盘确认按钮时是否保持键盘不收起
  159. confirmHold: {
  160. type: Boolean,
  161. default: false
  162. },
  163. // 设置键盘右下角按钮的文字
  164. confirmType: {
  165. type: String as PropType<"done" | "go" | "next" | "search" | "send">,
  166. default: "done"
  167. },
  168. // 键盘弹起时,是否自动上推页面
  169. adjustPosition: {
  170. type: Boolean,
  171. default: true
  172. },
  173. // 最大输入长度
  174. maxlength: {
  175. type: Number,
  176. default: 140
  177. },
  178. // 是否保持键盘不收起
  179. holdKeyboard: {
  180. type: Boolean,
  181. default: false
  182. }
  183. });
  184. // 事件定义
  185. const emit = defineEmits([
  186. "update:modelValue",
  187. "input",
  188. "change",
  189. "focus",
  190. "blur",
  191. "confirm",
  192. "clear",
  193. "keyboardheightchange"
  194. ]);
  195. // cl-form 上下文
  196. const { disabled } = useForm();
  197. // cl-form-item 上下文
  198. const { isError } = useFormItem();
  199. // 是否禁用
  200. const isDisabled = computed(() => {
  201. return disabled.value || props.disabled;
  202. });
  203. // 透传样式类型定义
  204. type PassThrough = {
  205. className?: string;
  206. inner?: PassThroughProps;
  207. prefixIcon?: ClIconProps;
  208. suffixIcon?: ClIconProps;
  209. };
  210. // 解析透传样式
  211. const pt = computed(() => parsePt<PassThrough>(props.pt));
  212. // 绑定值
  213. const value = ref<string>("");
  214. // 是否聚焦
  215. const isFocus = ref<boolean>(props.autofocus);
  216. // 是否显示清除按钮
  217. const showClear = computed(() => {
  218. return isFocus.value && props.clearable && value.value != "";
  219. });
  220. // 是否显示密码
  221. const isPassword = ref(props.password);
  222. // 切换密码显示状态
  223. function showPassword() {
  224. isPassword.value = !isPassword.value;
  225. }
  226. // 获取焦点事件
  227. function onFocus() {
  228. isFocus.value = true;
  229. emit("focus");
  230. }
  231. // 失去焦点事件
  232. function onBlur() {
  233. emit("blur");
  234. setTimeout(() => {
  235. isFocus.value = false;
  236. }, 0);
  237. }
  238. // 输入事件
  239. function onInput(e: UniInputEvent) {
  240. const val = e.detail.value;
  241. value.value = val;
  242. emit("update:modelValue", val);
  243. emit("input", val);
  244. if (val != value.value) {
  245. emit("change", val);
  246. }
  247. }
  248. // 点击确认按钮事件
  249. function onConfirm(e: UniInputConfirmEvent) {
  250. emit("confirm", e);
  251. }
  252. // 键盘高度变化事件
  253. function onKeyboardheightchange(e: UniInputKeyboardHeightChangeEvent) {
  254. emit("keyboardheightchange", e);
  255. }
  256. // 点击事件
  257. function onTap() {
  258. if (isDisabled.value) {
  259. return;
  260. }
  261. isFocus.value = true;
  262. }
  263. // 聚焦方法
  264. function focus() {
  265. setTimeout(() => {
  266. isFocus.value = false;
  267. nextTick(() => {
  268. isFocus.value = true;
  269. });
  270. }, 0);
  271. }
  272. // 清除方法
  273. function clear() {
  274. value.value = "";
  275. emit("update:modelValue", "");
  276. emit("change", "");
  277. emit("clear");
  278. // #ifdef H5
  279. focus();
  280. // #endif
  281. }
  282. watch(
  283. computed(() => props.modelValue),
  284. (val: string) => {
  285. value.value = val;
  286. },
  287. {
  288. immediate: true
  289. }
  290. );
  291. defineExpose({
  292. isFocus,
  293. focus,
  294. clear
  295. });
  296. </script>
  297. <style lang="scss" scoped>
  298. .cl-input {
  299. @apply flex flex-row items-center bg-white duration-200;
  300. @apply rounded-lg;
  301. height: 66rpx;
  302. padding: 0 20rpx;
  303. transition-property: background-color, border-color;
  304. &__inner {
  305. @apply h-full text-surface-700;
  306. flex: 1;
  307. font-size: 28rpx;
  308. &.is-dark {
  309. @apply text-white;
  310. }
  311. }
  312. &__icon {
  313. @apply flex items-center justify-center h-full;
  314. padding-left: 20rpx;
  315. }
  316. &--border {
  317. @apply border border-solid border-surface-200;
  318. }
  319. &--disabled {
  320. @apply bg-surface-100 opacity-70;
  321. }
  322. &--focus {
  323. &.cl-input--border {
  324. @apply border-primary-500;
  325. }
  326. }
  327. &--error {
  328. @apply border-red-500;
  329. }
  330. &.is-dark {
  331. @apply bg-surface-800;
  332. &.cl-input--border {
  333. @apply border-surface-700;
  334. &.cl-input--focus {
  335. @apply border-primary-500;
  336. }
  337. }
  338. &.cl-input--disabled {
  339. @apply bg-surface-700;
  340. }
  341. }
  342. }
  343. </style>