cl-select.uvue 7.9 KB

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