cl-select-time.uvue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <template>
  2. <cl-select-trigger
  3. v-if="showTrigger"
  4. :pt="ptTrigger"
  5. :placeholder="placeholder"
  6. :disabled="disabled"
  7. :focus="popupRef?.isOpen"
  8. :text="text"
  9. arrow-icon="time-line"
  10. @open="open()"
  11. @clear="clear"
  12. ></cl-select-trigger>
  13. <cl-popup ref="popupRef" v-model="visible" :title="title" :pt="ptPopup">
  14. <view class="cl-select-popup" @touchmove.stop>
  15. <view class="cl-select-popup__picker">
  16. <cl-picker-view
  17. :value="indexes"
  18. :headers="headers"
  19. :columns="columns"
  20. :reset-on-change="false"
  21. @change-index="onChange"
  22. ></cl-picker-view>
  23. </view>
  24. <view class="cl-select-popup__op">
  25. <cl-button
  26. v-if="showCancel"
  27. size="large"
  28. text
  29. border
  30. type="light"
  31. :pt="{
  32. className: 'flex-1 !rounded-xl h-[80rpx]'
  33. }"
  34. @tap="close"
  35. >{{ cancelText }}</cl-button
  36. >
  37. <cl-button
  38. v-if="showConfirm"
  39. size="large"
  40. :pt="{
  41. className: 'flex-1 !rounded-xl h-[80rpx]'
  42. }"
  43. @tap="confirm"
  44. >{{ confirmText }}</cl-button
  45. >
  46. </view>
  47. </view>
  48. </cl-popup>
  49. </template>
  50. <script setup lang="ts">
  51. import { ref, computed, type PropType, watch } from "vue";
  52. import type { ClSelectOption } from "../../types";
  53. import { isEmpty, isNull, parsePt, parseToObject } from "@/cool";
  54. import type { ClSelectTriggerPassThrough } from "../cl-select-trigger/props";
  55. import type { ClPopupPassThrough } from "../cl-popup/props";
  56. import { t } from "@/locale";
  57. defineOptions({
  58. name: "cl-select-time"
  59. });
  60. // 组件属性定义
  61. const props = defineProps({
  62. // 透传样式配置
  63. pt: {
  64. type: Object,
  65. default: () => ({})
  66. },
  67. // 选择器的值
  68. modelValue: {
  69. type: String,
  70. default: ""
  71. },
  72. // 表头
  73. headers: {
  74. type: Array as PropType<string[]>,
  75. default: () => [t("小时"), t("分钟"), t("秒数")]
  76. },
  77. // 类型,控制选择的粒度
  78. type: {
  79. type: String as PropType<"hour" | "minute" | "second">,
  80. default: "second"
  81. },
  82. // 选择器标题
  83. title: {
  84. type: String,
  85. default: () => t("请选择")
  86. },
  87. // 选择器占位符
  88. placeholder: {
  89. type: String,
  90. default: () => t("请选择")
  91. },
  92. // 是否显示选择器触发器
  93. showTrigger: {
  94. type: Boolean,
  95. default: true
  96. },
  97. // 是否禁用选择器
  98. disabled: {
  99. type: Boolean,
  100. default: false
  101. },
  102. // 确认按钮文本
  103. confirmText: {
  104. type: String,
  105. default: () => t("确定")
  106. },
  107. // 是否显示确认按钮
  108. showConfirm: {
  109. type: Boolean,
  110. default: true
  111. },
  112. // 取消按钮文本
  113. cancelText: {
  114. type: String,
  115. default: () => t("取消")
  116. },
  117. // 是否显示取消按钮
  118. showCancel: {
  119. type: Boolean,
  120. default: true
  121. },
  122. // 时间标签格式化
  123. labelFormat: {
  124. type: String as PropType<string | null>,
  125. default: null
  126. }
  127. });
  128. // 定义事件
  129. const emit = defineEmits(["update:modelValue", "change"]);
  130. // 弹出层引用
  131. const popupRef = ref<ClPopupComponentPublicInstance | null>(null);
  132. // 透传样式类型定义
  133. type PassThrough = {
  134. trigger?: ClSelectTriggerPassThrough;
  135. popup?: ClPopupPassThrough;
  136. };
  137. // 解析透传样式配置
  138. const pt = computed(() => parsePt<PassThrough>(props.pt));
  139. // 解析触发器透传样式配置
  140. const ptTrigger = computed(() => parseToObject(pt.value.trigger));
  141. // 解析弹窗透传样式配置
  142. const ptPopup = computed(() => parseToObject(pt.value.popup));
  143. // 标签格式化
  144. const labelFormat = computed(() => {
  145. if (props.labelFormat != null) {
  146. return props.labelFormat;
  147. }
  148. if (props.type == "hour") {
  149. return "{H}";
  150. }
  151. if (props.type == "minute") {
  152. return "{H}:{m}";
  153. }
  154. return "{H}:{m}:{s}";
  155. });
  156. // 当前选中的值
  157. const value = ref<string[]>([]);
  158. // 当前选中项的索引
  159. const indexes = ref<number[]>([]);
  160. // 时间选择器列表
  161. const list = computed(() => {
  162. const arr = [[], [], []] as ClSelectOption[][];
  163. for (let i = 0; i < 60; i++) {
  164. const v = i.toString().padStart(2, "0");
  165. const item = {
  166. label: v,
  167. value: v
  168. } as ClSelectOption;
  169. if (i < 24) {
  170. arr[0].push(item);
  171. }
  172. arr[1].push(item);
  173. arr[2].push(item);
  174. }
  175. return arr;
  176. });
  177. // 列数,决定显示多少列(时、分、秒)
  178. const columnNum = computed(() => {
  179. return ["hour", "minute", "second"].findIndex((e) => e == props.type) + 1;
  180. });
  181. // 列数据,取出需要显示的列
  182. const columns = computed(() => {
  183. return list.value.slice(0, columnNum.value);
  184. });
  185. // 显示文本
  186. const text = ref("");
  187. // 更新文本内容
  188. function updateText() {
  189. // 获取当前v-model绑定的时间字符串
  190. const val = props.modelValue;
  191. // 如果值为空或为null,返回空字符串
  192. if (isEmpty(val) || isNull(val)) {
  193. text.value = "";
  194. } else {
  195. // 拆分时间字符串,分别获取小时、分钟、秒
  196. const [h, m, s] = val.split(":");
  197. // 按照labelFormat格式化显示文本
  198. text.value = labelFormat.value.replace("{H}", h).replace("{m}", m).replace("{s}", s);
  199. }
  200. }
  201. // 设置值
  202. function setValue(val: string) {
  203. // 声明选中值数组
  204. let _value: string[];
  205. // 判断输入值是否为空
  206. if (isEmpty(val) || isNull(val)) {
  207. // 设置空数组
  208. _value = [];
  209. } else {
  210. // 按冒号分割字符串为数组
  211. _value = val.split(":");
  212. }
  213. // 声明索引数组
  214. let _indexes = [] as number[];
  215. // 遍历时分秒三列
  216. for (let i = 0; i < 3; i++) {
  217. // 判断是否需要设置默认值
  218. if (i >= _value.length) {
  219. // 添加默认索引0
  220. _indexes.push(0);
  221. // 添加默认值
  222. _value.push(list.value[i][0].value as string);
  223. } else {
  224. // 查找当前值对应的索引
  225. let index = list.value[i].findIndex((e) => e.value == _value[i]);
  226. // 索引无效时重置为0
  227. if (index < 0) {
  228. index = 0;
  229. }
  230. // 添加索引
  231. _indexes.push(index);
  232. }
  233. }
  234. // 更新选中值
  235. value.value = _value;
  236. // 更新索引值
  237. indexes.value = _indexes;
  238. // 更新文本内容
  239. updateText();
  240. }
  241. // 选择器值改变事件
  242. function onChange(a: number[]) {
  243. // 复制当前组件内部维护的索引数组
  244. const b = [...indexes.value];
  245. // 遍历所有列,处理联动逻辑
  246. for (let i = 0; i < a.length; i++) {
  247. // 检查当前列是否发生改变
  248. if (b[i] != a[i]) {
  249. // 更新当前列的索引
  250. b[i] = a[i];
  251. }
  252. }
  253. // 更新组件内部维护的索引数组
  254. indexes.value = b;
  255. // 根据最新的索引数组,更新选中的值数组
  256. value.value = b.map((e, i) => list.value[i][e].value as string);
  257. }
  258. // 选择器显示状态
  259. const visible = ref(false);
  260. // 选择回调函数
  261. let callback: ((value: string) => void) | null = null;
  262. // 打开选择器
  263. function open(cb: ((value: string) => void) | null = null) {
  264. if (props.disabled) {
  265. return;
  266. }
  267. // 显示选择器弹窗
  268. visible.value = true;
  269. // 设置值
  270. setValue(props.modelValue);
  271. // 保存回调函数
  272. callback = cb;
  273. }
  274. // 关闭选择器
  275. function close() {
  276. visible.value = false;
  277. }
  278. // 清空选择器
  279. function clear() {
  280. text.value = "";
  281. emit("update:modelValue", "");
  282. emit("change", "");
  283. }
  284. // 确认选择
  285. function confirm() {
  286. // 将选中值转换为字符串格式
  287. const val = value.value.join(":");
  288. // 触发更新事件
  289. emit("update:modelValue", val);
  290. emit("change", val);
  291. // 触发回调
  292. if (callback != null) {
  293. callback!(val);
  294. }
  295. // 关闭选择器
  296. close();
  297. }
  298. // 监听modelValue变化
  299. watch(
  300. computed(() => props.modelValue),
  301. (val: string) => {
  302. setValue(val);
  303. },
  304. {
  305. immediate: true
  306. }
  307. );
  308. // 监听labelFormat和type变化
  309. watch(
  310. computed(() => [props.labelFormat, props.type]),
  311. () => {
  312. updateText();
  313. }
  314. );
  315. defineExpose({
  316. open,
  317. close
  318. });
  319. </script>
  320. <style lang="scss" scoped>
  321. .cl-select {
  322. &-popup {
  323. &__op {
  324. @apply flex flex-row items-center justify-center;
  325. padding: 24rpx;
  326. }
  327. }
  328. }
  329. </style>