cl-select.uvue 8.1 KB

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