cl-keyboard-number.uvue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <template>
  2. <cl-popup
  3. :title="title"
  4. :swipe-close-threshold="100"
  5. :pt="{
  6. inner: {
  7. className: parseClass([
  8. [isDark, '!bg-surface-700', '!bg-surface-100'],
  9. pt.popup?.className
  10. ])
  11. },
  12. mask: {
  13. className: '!bg-transparent'
  14. }
  15. }"
  16. v-model="visible"
  17. >
  18. <view class="cl-keyboard-number" :class="[pt.className]">
  19. <slot name="value" :value="value">
  20. <view
  21. v-if="showValue"
  22. class="cl-keyboard-number__value"
  23. :class="[pt.value?.className]"
  24. >
  25. <cl-text
  26. v-if="value != ''"
  27. :pt="{
  28. className: 'text-2xl'
  29. }"
  30. >{{ value }}</cl-text
  31. >
  32. <cl-text
  33. v-else
  34. :pt="{
  35. className: 'text-md text-surface-400'
  36. }"
  37. >{{ placeholder }}</cl-text
  38. >
  39. </view>
  40. </slot>
  41. <view class="cl-keyboard-number__list">
  42. <cl-row :gutter="10">
  43. <cl-col :span="18">
  44. <cl-row :gutter="10">
  45. <cl-col :span="8" v-for="item in list" :key="item">
  46. <view
  47. class="cl-keyboard-number__item"
  48. :class="[
  49. `is-keycode-${item}`,
  50. {
  51. 'is-dark': isDark,
  52. 'is-empty': item == ''
  53. },
  54. pt.item?.className
  55. ]"
  56. hover-class="opacity-50"
  57. :hover-stay-time="250"
  58. @touchstart.stop="onCommand(item)"
  59. >
  60. <slot name="item" :item="item">
  61. <cl-icon
  62. v-if="item == 'delete'"
  63. name="delete-back-2-line"
  64. :size="36"
  65. ></cl-icon>
  66. <view
  67. v-else-if="item == 'confirm'"
  68. class="cl-keyboard-number__item-confirm"
  69. >
  70. <cl-text
  71. color="white"
  72. :pt="{
  73. className: 'text-lg'
  74. }"
  75. >{{ confirmText }}</cl-text
  76. >
  77. </view>
  78. <view v-else-if="item == '_confirm'"></view>
  79. <cl-text
  80. v-else
  81. :pt="{
  82. className: 'text-lg'
  83. }"
  84. >{{ item }}</cl-text
  85. >
  86. </slot>
  87. </view>
  88. </cl-col>
  89. </cl-row>
  90. </cl-col>
  91. <cl-col :span="6">
  92. <view class="cl-keyboard-number__op">
  93. <view
  94. v-for="item in opList"
  95. :key="item"
  96. class="cl-keyboard-number__item"
  97. :class="[
  98. {
  99. 'is-dark': isDark
  100. },
  101. `is-keycode-${item}`
  102. ]"
  103. hover-class="opacity-50"
  104. :hover-stay-time="250"
  105. @touchstart.stop="onCommand(item)"
  106. >
  107. <cl-icon
  108. v-if="item == 'delete'"
  109. name="delete-back-2-line"
  110. :size="36"
  111. ></cl-icon>
  112. <cl-text
  113. v-if="item == 'confirm'"
  114. color="white"
  115. :pt="{
  116. className: 'text-lg'
  117. }"
  118. >{{ confirmText }}</cl-text
  119. >
  120. </view>
  121. </view>
  122. </cl-col>
  123. </cl-row>
  124. </view>
  125. </view>
  126. </cl-popup>
  127. </template>
  128. <script setup lang="ts">
  129. import { useUi } from "../../hooks";
  130. import type { PassThroughProps } from "../../types";
  131. import type { ClPopupProps } from "../cl-popup/props";
  132. import { ref, computed, watch, type PropType } from "vue";
  133. import { $t, t } from "@/locale";
  134. import { isAppIOS, isDark, parseClass, parsePt } from "@/cool";
  135. import { vibrate } from "@/uni_modules/cool-vibrate";
  136. defineOptions({
  137. name: "cl-keyboard-number"
  138. });
  139. defineSlots<{
  140. value(props: { value: string }): any;
  141. item(props: { item: string }): any;
  142. }>();
  143. const props = defineProps({
  144. // 透传样式配置
  145. pt: {
  146. type: Object,
  147. default: () => ({})
  148. },
  149. // v-model绑定的值
  150. modelValue: {
  151. type: String,
  152. default: ""
  153. },
  154. // 键盘类型,支持number、digit、idcard
  155. type: {
  156. type: String as PropType<"number" | "digit" | "idcard">,
  157. default: "digit"
  158. },
  159. // 弹窗标题
  160. title: {
  161. type: String,
  162. default: () => t("数字键盘")
  163. },
  164. // 输入框占位符
  165. placeholder: {
  166. type: String,
  167. default: () => t("安全键盘,请放心输入")
  168. },
  169. // 最大输入长度
  170. maxlength: {
  171. type: Number,
  172. default: 10
  173. },
  174. // 确认按钮文本
  175. confirmText: {
  176. type: String,
  177. default: () => t("确定")
  178. },
  179. // 是否显示输入值
  180. showValue: {
  181. type: Boolean,
  182. default: true
  183. },
  184. // 是否输入即绑定
  185. inputImmediate: {
  186. type: Boolean,
  187. default: false
  188. }
  189. });
  190. // 定义事件发射器,支持v-model和change事件
  191. const emit = defineEmits(["update:modelValue", "change"]);
  192. // 样式穿透类型
  193. type PassThrough = {
  194. className?: string;
  195. item?: PassThroughProps;
  196. value?: PassThroughProps;
  197. popup?: ClPopupProps;
  198. };
  199. // 样式穿透计算
  200. const pt = computed(() => parsePt<PassThrough>(props.pt));
  201. // 获取UI相关的工具方法
  202. const ui = useUi();
  203. // 控制弹窗显示/隐藏
  204. const visible = ref(false);
  205. // 输入框当前值,双向绑定
  206. const value = ref(props.modelValue);
  207. // 最大输入长度
  208. const maxlength = computed(() => {
  209. if (props.type == "idcard") {
  210. return 18;
  211. }
  212. return props.maxlength;
  213. });
  214. // 数字键盘的按键列表,包含数字、删除、00和小数点
  215. const list = computed(() => {
  216. const arr = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "00", "0", ""];
  217. // 数字键盘显示为小数点 "."
  218. if (props.type == "digit") {
  219. arr[11] = ".";
  220. }
  221. // 身份证键盘显示为 "X"
  222. if (props.type == "idcard") {
  223. arr[11] = "X";
  224. }
  225. return arr;
  226. });
  227. // 操作按钮列表
  228. const opList = computed(() => {
  229. return ["delete", "confirm"];
  230. });
  231. // 打开键盘弹窗
  232. function open() {
  233. visible.value = true;
  234. }
  235. // 关闭键盘弹窗
  236. function close() {
  237. visible.value = false;
  238. }
  239. // 处理键盘按键点击事件
  240. function onCommand(key: string) {
  241. // 震动
  242. try {
  243. vibrate(1);
  244. } catch (error) {}
  245. // 确认按钮逻辑
  246. if (key == "confirm" || key == "_confirm") {
  247. if (value.value == "") {
  248. ui.showToast({
  249. message: t("请输入内容")
  250. });
  251. return;
  252. }
  253. // 如果最后一位是小数点,去掉
  254. if (value.value.endsWith(".")) {
  255. value.value = value.value.slice(0, -1);
  256. }
  257. // 身份证号码正则校验(支持15位和18位,18位末尾可为X/x)
  258. if (props.type == "idcard") {
  259. if (
  260. !/^(^[1-9]\d{5}(18|19|20)?\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}(\d|X|x)?$)$/.test(
  261. value.value
  262. )
  263. ) {
  264. ui.showToast({
  265. message: t("身份证号码格式不正确")
  266. });
  267. return;
  268. }
  269. }
  270. // 触发v-model和change事件
  271. emit("update:modelValue", value.value);
  272. emit("change", value.value);
  273. // 关闭弹窗
  274. close();
  275. return;
  276. }
  277. // 删除键,去掉最后一位
  278. if (key == "delete") {
  279. value.value = value.value.slice(0, -1);
  280. return;
  281. }
  282. // 超过最大输入长度,提示并返回
  283. if (value.value.length >= maxlength.value) {
  284. ui.showToast({
  285. message: $t("最多输入{maxlength}位", {
  286. maxlength: maxlength.value
  287. })
  288. });
  289. return;
  290. }
  291. // 处理小数点输入,已存在则不再添加
  292. if (key == ".") {
  293. if (value.value.includes(".")) {
  294. return;
  295. }
  296. if (value.value == "") {
  297. value.value = "0.";
  298. return;
  299. }
  300. }
  301. // 处理00键,首位不能输入00,只能输入0
  302. if (key == "00") {
  303. if (value.value.length + 2 > maxlength.value) {
  304. value.value += "0";
  305. return;
  306. }
  307. if (value.value == "") {
  308. value.value = "0";
  309. return;
  310. }
  311. }
  312. if (key == "00" || key == "0") {
  313. if (value.value == "" || value.value == "0") {
  314. value.value = "0";
  315. return;
  316. }
  317. }
  318. // 其他按键直接拼接到value
  319. value.value += key;
  320. }
  321. watch(value, (val: string) => {
  322. // 如果输入即绑定,则立即更新绑定值
  323. if (props.inputImmediate) {
  324. emit("update:modelValue", val);
  325. emit("change", val);
  326. }
  327. });
  328. // 监听外部v-model的变化,保持内部value同步
  329. watch(
  330. computed(() => props.modelValue),
  331. (val: string) => {
  332. value.value = val;
  333. }
  334. );
  335. defineExpose({
  336. open,
  337. close
  338. });
  339. </script>
  340. <style lang="scss" scoped>
  341. .cl-keyboard-number {
  342. padding: 0 20rpx 20rpx 20rpx;
  343. &__value {
  344. @apply flex flex-row items-center justify-center;
  345. height: 80rpx;
  346. margin-bottom: 20rpx;
  347. }
  348. &__list {
  349. @apply relative overflow-visible;
  350. }
  351. &__op {
  352. @apply flex flex-col h-full;
  353. }
  354. &__item {
  355. @apply flex items-center justify-center rounded-xl bg-white overflow-visible;
  356. height: 100rpx;
  357. margin-top: 10rpx;
  358. &.is-dark {
  359. @apply bg-surface-800;
  360. }
  361. &.is-keycode-delete {
  362. @apply bg-surface-200;
  363. &.is-dark {
  364. @apply bg-surface-800;
  365. }
  366. }
  367. &.is-keycode-confirm {
  368. @apply flex flex-col items-center justify-center;
  369. @apply bg-primary-500 rounded-xl flex-1;
  370. }
  371. &.is-empty {
  372. background-color: transparent !important;
  373. }
  374. }
  375. }
  376. </style>