cl-select-time.uvue 7.3 KB

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