cl-select.uvue 7.9 KB

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