云
4 小时以前 fb4db4ae9b6a7f113ba049f6789e6916c0dcc276
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onDeactivated, ref, useId } from 'vue';
import Icon from '#/mobile/icon.vue';
 
export interface Choice { id?: number; name?: string; code?: string }
const props = withDefaults(defineProps<{
  modelValue?: number; label: string; options: Choice[]; disabled?: boolean;
  loading?: boolean; placeholder?: string; allowEmpty?: boolean;
}>(), { placeholder: '请选择', allowEmpty: false });
const emit = defineEmits<{ 'update:modelValue': [value: number | undefined] }>();
const dialog = ref<HTMLDialogElement>(), searchInput = ref<HTMLInputElement>();
const search = ref(''), opened = ref(false), titleId = useId();
const selected = computed(() => props.options.find(o => o.id === props.modelValue));
const filtered = computed(() => {
  const key = search.value.trim().toLocaleLowerCase();
  return props.options.filter(o => o.id != null && (!key || [o.name, o.code].some(v => v?.toLocaleLowerCase().includes(key))));
});
let previousOverflow = '';
async function open() {
  if (props.disabled || props.loading || opened.value) return;
  search.value = ''; opened.value = true;
  previousOverflow = document.body.style.overflow;
  document.body.style.overflow = 'hidden';
  await nextTick();
  dialog.value?.showModal();
  searchInput.value?.focus({ preventScroll: true });
}
function close() {
  if (!opened.value) return;
  dialog.value?.close(); opened.value = false;
  document.body.style.overflow = previousOverflow;
}
function choose(value?: number) { emit('update:modelValue', value); close(); }
function trapFocus(event: KeyboardEvent) {
  if(event.key!=='Tab')return;
  const controls=Array.from(dialog.value?.querySelectorAll<HTMLElement>('button:not(:disabled),input:not(:disabled)')||[])
    .filter(el=>el.getClientRects().length>0);
  const first=controls[0],last=controls[controls.length-1];
  if(!first||!last)return;
  if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}
  else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();}
}
function backdrop(e: MouseEvent) {
  if (e.target !== dialog.value) return;
  const rect = dialog.value.getBoundingClientRect();
  if (e.clientY < rect.top || e.clientX < rect.left || e.clientX > rect.right) close();
}
onBeforeUnmount(close);
onDeactivated(close);
</script>
<template>
  <div class="m-field">
    <span :id="titleId+'-label'">{{label}}</span>
    <button type="button" class="m-picker-trigger" :disabled="disabled||loading" aria-haspopup="dialog"
      :aria-labelledby="titleId+'-label '+titleId+'-value'" :aria-expanded="opened" @click="open">
      <span :id="titleId+'-value'">{{loading?'正在加载…':selected?.name||placeholder}}</span><Icon name="arrow"/>
    </button>
  </div>
  <dialog ref="dialog" class="m-picker-dialog" :aria-labelledby="titleId" @cancel.prevent="close" @click="backdrop" @keydown="trapFocus">
    <div class="m-picker-body">
      <header class="m-picker-header"><h2 :id="titleId" class="m-title">选择{{label}}</h2><button type="button" class="m-icon-button" :aria-label="'关闭'+label+'选择'" @click="close">×</button></header>
      <label class="m-field"><span>搜索{{label}}</span><input ref="searchInput" v-model="search" class="m-input" :placeholder="'输入'+label+'名称或编码'" autocomplete="off"/></label>
      <div class="m-picker-options" :aria-label="label+'选项'">
        <button v-if="allowEmpty" type="button" class="m-picker-option" :aria-pressed="modelValue==null" @click="choose()">{{placeholder}}<span v-if="modelValue==null">✓</span></button>
        <button v-for="option in filtered" :key="option.id" type="button" class="m-picker-option" :aria-pressed="option.id===modelValue" @click="choose(option.id)">
          <span>{{option.name||option.code||option.id}}<small v-if="option.code&&option.code!==option.name">{{option.code}}</small></span><span v-if="option.id===modelValue">✓</span>
        </button>
        <p v-if="!filtered.length" class="m-empty">{{options.length?'没有匹配的'+label:'暂无可选'+label}}</p>
      </div>
    </div>
  </dialog>
</template>