ut-action-sheet.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <template>
  2. <template v-if="mode === 'picker'">
  3. <picker :range="options" range-key="name" @change="selectChange" :disabled="false">
  4. <slot></slot>
  5. </picker>
  6. </template>
  7. <template v-else-if="mode === 'custom'">
  8. <view @click="showModel = true" class="flex1">
  9. <slot></slot>
  10. </view>
  11. <up-popup v-model:show="showModel" mode="center" round="30rpx" :safeAreaInsetBottom="false" closeable @close="showModel = false">
  12. <view class="w-700">
  13. <view class="pd-24">
  14. <view class="f-s-32 c-#333 f-w-500">{{ title }}</view>
  15. </view>
  16. <scroll-view scroll-y style="max-height: 70vh">
  17. <view class="pd3-10-24-24">
  18. <ut-row gap="16rpx">
  19. <template v-for="(item, index) in options" :key="index">
  20. <ut-col :span="item.span">
  21. <view @click="clickCol(item)" class="ut-custom-item-sheet p-rtv" :class="{ active: checkeds[item.value] }"
  22. >{{ item?.remark || item?.name }}
  23. <image v-if="checkeds[item.value]" class="w-40 h-40 checked-icon" src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images-lm/price/checked1.png" mode="widthFix" />
  24. </view>
  25. </ut-col>
  26. </template>
  27. </ut-row>
  28. </view>
  29. </scroll-view>
  30. <view v-if="multiple" class="d-flex j-c pd-24">
  31. <up-button class="mr-20" @click="onCancel">取消</up-button>
  32. <up-button type="primary" @click="onConfirm">确定</up-button>
  33. </view>
  34. </view>
  35. </up-popup>
  36. </template>
  37. </template>
  38. <script setup lang="ts">
  39. import { ref, watch, computed } from 'vue';
  40. const props = defineProps({
  41. modelValue: {
  42. // 支持数组或字符串:多选时根据现有类型返回对应格式
  43. type: [Array, String],
  44. default: () => [],
  45. },
  46. title: {
  47. type: String,
  48. default: '系统提示',
  49. },
  50. showTitle: {
  51. type: Boolean,
  52. default: true,
  53. },
  54. tabs: {
  55. type: Array,
  56. default: () => [],
  57. },
  58. mode: {
  59. type: String,
  60. default: 'picker', // 原生 picker 模式 或自定义弹窗模式 custom
  61. },
  62. // 是否多选
  63. multiple: {
  64. type: Boolean,
  65. default: false, // 只有自定义弹窗模式下生效
  66. },
  67. valueType: {
  68. type: String,
  69. default: 'string', // 数组 array 或 字符串 string,只有多选时生效
  70. },
  71. });
  72. const options = computed(() => {
  73. return props.tabs.map((item: any) => {
  74. // 如果 elTagClass正则表达式匹配的是数字,则作为span使用,否则默认10,如果是个对象字符,取span属性值
  75. let span = 10;
  76. // Unexpected end of JSON input
  77. // at JSON.parse (<anonymous>)
  78. if (item.elTagClass) {
  79. const num = Number(item.elTagClass);
  80. if (!isNaN(num)) {
  81. span = num;
  82. } else {
  83. try {
  84. const obj = JSON.parse(item.elTagClass);
  85. if (obj && obj.span) {
  86. span = obj.span;
  87. }
  88. } catch (e) {
  89. // do nothing
  90. }
  91. }
  92. }
  93. return {
  94. name: item.label,
  95. value: item.value,
  96. span,
  97. };
  98. });
  99. });
  100. const showModel = ref(false);
  101. const emit = defineEmits(['close', 'confirm', 'open', 'update:show', 'update:modelValue', 'change']);
  102. const checkeds = ref<any>({});
  103. // 未选择之前的
  104. const initialCheckeds = ref<any>({});
  105. const close = () => {
  106. showModel.value = false;
  107. // 如果有初始值,恢复初始值
  108. checkeds.value = { ...initialCheckeds.value };
  109. emit('update:show', false);
  110. emit('close');
  111. };
  112. // 获取当前已选中的值列表
  113. const getSelectedValues = () => {
  114. return Object.keys(checkeds.value).filter((key) => checkeds.value[key]);
  115. };
  116. const clickCol = (item: any) => {
  117. // 判断是单选还是多选
  118. if (props.multiple) {
  119. // 多选
  120. if (checkeds.value[item.value]) {
  121. // 取消选择
  122. delete checkeds.value[item.value];
  123. } else {
  124. // 选择
  125. checkeds.value[item.value] = true;
  126. }
  127. } else {
  128. // 单选
  129. checkeds.value = {
  130. [item.value]: true,
  131. };
  132. emit('update:modelValue', item.value);
  133. emit('change', item.value);
  134. close();
  135. }
  136. };
  137. // 多选确认
  138. const onConfirm = () => {
  139. const values = getSelectedValues();
  140. let out: any;
  141. if (Array.isArray(props.modelValue) || props.valueType === 'array') {
  142. out = values;
  143. } else {
  144. // 字符串场景下,用逗号拼接
  145. out = values.join(',');
  146. }
  147. emit('update:modelValue', out);
  148. emit('change', out);
  149. // 确认后更新初始选中值
  150. initialCheckeds.value = { ...checkeds.value };
  151. close();
  152. };
  153. // 多选取消
  154. const onCancel = () => {
  155. close();
  156. };
  157. const selectChange = (item: any) => {
  158. const selectedValues = options.value[item.detail.value].value;
  159. emit('update:modelValue', selectedValues);
  160. emit('change', selectedValues);
  161. close();
  162. };
  163. watch(
  164. () => props.modelValue,
  165. (newVal: any) => {
  166. // 初始化已选择项
  167. let selected: any = {};
  168. if (props.multiple) {
  169. if (Array.isArray(newVal)) {
  170. newVal.forEach((val: any) => {
  171. selected[val] = true;
  172. });
  173. } else if (typeof newVal === 'string' && newVal) {
  174. newVal
  175. .split(/[,,]/)
  176. .map((val: string) => val.trim())
  177. .filter((val: string) => !!val)
  178. .forEach((val: string) => {
  179. selected[val] = true;
  180. });
  181. }
  182. } else if (newVal != null) {
  183. selected = {
  184. [newVal as string]: true,
  185. };
  186. }
  187. checkeds.value = selected;
  188. initialCheckeds.value = { ...selected };
  189. },
  190. { immediate: true },
  191. );
  192. </script>
  193. <script lang="ts">
  194. export default {
  195. options: {
  196. // 微信小程序中 options 选项
  197. multipleSlots: true, // 在组件定义时的选项中启动多slot支持,默认启用
  198. styleIsolation: 'shared', // 启动样式隔离。当使用页面自定义组件,希望父组件影响子组件样式时可能需要配置。具体配置选项参见:微信小程序自定义组件的样式
  199. addGlobalClass: true, // 表示页面样式将影响到自定义组件,但自定义组件中指定的样式不会影响页面。这个选项等价于设置 styleIsolation: apply-shared
  200. virtualHost: true // 将自定义节点设置成虚拟的,更加接近Vue组件的表现。我们不希望自定义组件的这个节点本身可以设置样式、响应 flex 布局等,而是希望自定义组件内部的第一层节点能够响应 flex 布局或者样式由自定义组件本身完全决定
  201. }
  202. }
  203. </script>
  204. <style lang="scss" scoped>
  205. .ut-custom-item-sheet {
  206. padding: 18rpx 6rpx;
  207. border: 1rpx solid #f7f7f7;
  208. background-color: #f7f7f7;
  209. border-radius: 10rpx;
  210. text-align: center;
  211. font-size: 30rpx;
  212. color: #333;
  213. &.active {
  214. border-color: #37a954;
  215. background-color: #e6f4f0;
  216. color: #37a954;
  217. }
  218. }
  219. .checked-icon {
  220. position: absolute;
  221. right: 0;
  222. bottom: 0;
  223. }
  224. </style>