UPickerField.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <template>
  2. <view class="u-picker-field" @click="openPicker" :class="{ disabled }">
  3. <view class="field-left">
  4. <text class="field-text" :class="{ placeholder: !selectedText }">
  5. {{ selectedText || placeholder }}
  6. </text>
  7. </view>
  8. <view class="field-right">
  9. <up-icon
  10. name="arrow-down"
  11. size="16"
  12. color="var(--text-02)"
  13. :style="{
  14. transform: visible ? 'rotate(180deg)' : 'rotate(0deg)',
  15. transition: 'transform .2s ease',
  16. }"
  17. />
  18. </view>
  19. <!-- up-picker -->
  20. <up-picker
  21. v-model:show="visible"
  22. :columns="normalizedColumns"
  23. :loading="loading"
  24. :valueName="valueKey"
  25. :keyName="labelKey"
  26. :item-height="itemHeight"
  27. :confirm-text="t(confirmText)"
  28. :cancel-text="t(cancelText)"
  29. @confirm="onConfirm"
  30. @cancel="onCancel"
  31. @close="onClose"
  32. />
  33. </view>
  34. </template>
  35. <script setup>
  36. import { computed, ref, watch } from "vue";
  37. import { t } from "@/locale";
  38. const props = defineProps({
  39. // v-model
  40. modelValue: {
  41. type: [String, Number, Object, Array],
  42. default: "",
  43. },
  44. // up-picker 的 columns(支持一列或多列)
  45. columns: {
  46. type: Array,
  47. default: () => [],
  48. },
  49. // 当 columns 为对象数组时的显示/取值字段
  50. labelKey: {
  51. type: String,
  52. default: "label",
  53. },
  54. valueKey: {
  55. type: String,
  56. default: "value",
  57. },
  58. placeholder: {
  59. type: String,
  60. default: "",
  61. },
  62. disabled: {
  63. type: Boolean,
  64. default: false,
  65. },
  66. loading: {
  67. type: Boolean,
  68. default: false,
  69. },
  70. itemHeight: {
  71. type: Number,
  72. default: 44,
  73. },
  74. confirmText: {
  75. type: String,
  76. default: "确认",
  77. },
  78. cancelText: {
  79. type: String,
  80. default: "取消",
  81. },
  82. // 是否在值为空时自动选中第一项
  83. autoSelectFirst: {
  84. type: Boolean,
  85. default: true,
  86. },
  87. });
  88. const emit = defineEmits(["update:modelValue", "change", "open", "close"]);
  89. const visible = ref(false);
  90. // 统一成多列结构:[[...]]
  91. const normalizedColumns = computed(() => {
  92. if (props.columns.length === 0) return [];
  93. const isMulti = Array.isArray(props.columns[0]);
  94. return isMulti ? props.columns : [props.columns];
  95. });
  96. // 如果 modelValue 为空且有可选项,则默认选中第一项(受 autoSelectFirst 控制)
  97. watch(
  98. () => normalizedColumns.value,
  99. (cols) => {
  100. if (!props.autoSelectFirst) return;
  101. if (!cols || !cols.length) return;
  102. const mv = props.modelValue;
  103. const isEmpty = mv === undefined || mv === null || mv === "";
  104. if (!isEmpty) return;
  105. const firstCol = cols[0] || [];
  106. const first = firstCol[0];
  107. if (first === undefined) return;
  108. const value = typeof first === "object" ? first[props.valueKey] : first;
  109. const label =
  110. typeof first === "object" ? first[props.labelKey] ?? "" : String(first);
  111. emit("update:modelValue", value);
  112. emit("change", { value, label, raw: { type: "init" } });
  113. },
  114. { immediate: true, deep: true }
  115. );
  116. // 选中文本(以第一列为主,常见场景)
  117. const selectedText = computed(() => {
  118. const firstCol = normalizedColumns.value[0] || [];
  119. if (!firstCol.length) return "";
  120. // 根据 modelValue 匹配文本
  121. const mv = props.modelValue;
  122. if (mv === null || mv === undefined || mv === "") return "";
  123. // 如果是对象直接取 labelKey
  124. if (typeof mv === "object" && !Array.isArray(mv)) {
  125. return mv[props.labelKey] ?? "";
  126. }
  127. // 原始值:在第一列匹配
  128. const found = firstCol.find((item) => {
  129. if (typeof item === "object") return item[props.valueKey] === mv;
  130. return item === mv;
  131. });
  132. if (!found) return "";
  133. return typeof found === "object"
  134. ? found[props.labelKey] ?? ""
  135. : String(found);
  136. });
  137. const openPicker = () => {
  138. if (props.disabled) return;
  139. visible.value = true;
  140. emit("open");
  141. };
  142. const onClose = () => {
  143. emit("close");
  144. };
  145. const onCancel = () => {
  146. visible.value = false;
  147. };
  148. // up-picker 的 confirm 事件回调参数在不同版本可能不同,这里做兼容处理
  149. const onConfirm = (e) => {
  150. const firstCol = normalizedColumns.value[0] || [];
  151. let picked;
  152. let pickedDisplay = "";
  153. // 优先从 e.value 推断
  154. if (e && Array.isArray(e.value) && e.value.length) {
  155. const v0 = e.value[0];
  156. if (typeof v0 === "object") {
  157. picked = v0[props.valueKey];
  158. pickedDisplay = v0[props.labelKey] ?? "";
  159. } else {
  160. // v0 是 label,找到对应项拿到 value
  161. const found = firstCol.find((item) =>
  162. typeof item === "object" ? item[props.labelKey] === v0 : item === v0
  163. );
  164. if (found) {
  165. picked = typeof found === "object" ? found[props.valueKey] : found;
  166. pickedDisplay =
  167. typeof found === "object"
  168. ? found[props.labelKey] ?? ""
  169. : String(found);
  170. } else {
  171. picked = v0;
  172. pickedDisplay = String(v0);
  173. }
  174. }
  175. } else if (e && (Array.isArray(e.indexs) || typeof e.index === "number")) {
  176. const idx = Array.isArray(e.indexs) ? e.indexs[0] : e.index;
  177. const it = firstCol[idx];
  178. if (it) {
  179. picked = typeof it === "object" ? it[props.valueKey] : it;
  180. pickedDisplay =
  181. typeof it === "object" ? it[props.labelKey] ?? "" : String(it);
  182. }
  183. }
  184. // 回填 & 透传事件
  185. if (picked !== undefined) {
  186. emit("update:modelValue", picked);
  187. emit("change", { value: picked, label: pickedDisplay, raw: e });
  188. }
  189. visible.value = false;
  190. };
  191. </script>
  192. <style lang="less" scoped>
  193. .u-picker-field {
  194. display: flex;
  195. align-items: center;
  196. justify-content: space-between;
  197. padding: 16rpx 20rpx;
  198. background-color: var(--light);
  199. border-radius: 12rpx;
  200. &.disabled {
  201. opacity: 0.6;
  202. }
  203. .field-left {
  204. flex: 1;
  205. min-width: 0;
  206. .field-text {
  207. color: var(--text);
  208. font-size: 28rpx;
  209. line-height: 40rpx;
  210. overflow: hidden;
  211. white-space: nowrap;
  212. text-overflow: ellipsis;
  213. &.placeholder {
  214. color: var(--text-02);
  215. }
  216. }
  217. }
  218. .field-right {
  219. margin-left: 12rpx;
  220. display: flex;
  221. align-items: center;
  222. }
  223. }
  224. </style>