liu
4 天以前 ee7ac8df8bce38add1f75baf876b49de94b9e00a
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
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
 
const props = defineProps<{ bags: MesWmProductSalesApi.PickingBag[]; modelValue: number[]; disabled?: boolean }>();
const emit = defineEmits<{ 'update:modelValue': [ids: number[]] }>();
const search = ref(''), desired = ref<number>(), limit = ref(50), selectionError = ref('');
const eligible = computed(() => props.bags.filter(b => b.selectable));
const filtered = computed(() => props.bags.filter(b => !search.value || b.code.includes(search.value.trim()) || b.businessCode?.includes(search.value.trim())));
watch(search, () => { limit.value = 50; });
watch(() => props.modelValue, ids => { desired.value = ids.length || undefined; });
function choose(bag: MesWmProductSalesApi.PickingBag, checked: boolean) {
  selectionError.value = '';
  emit('update:modelValue', checked ? [...props.modelValue, bag.id] : props.modelValue.filter(id => id !== bag.id));
}
function chooseQuantity() {
  const count = Number(desired.value);
  if (!Number.isInteger(count) || count < 1 || count > eligible.value.length) {
    selectionError.value = `请输入 1 至 ${eligible.value.length} 的整数袋数`; return;
  }
  selectionError.value = '';
  emit('update:modelValue', eligible.value.slice(0, count).map(b => b.id));
}
</script>
<template>
  <fieldset class="pick-bags" :disabled="disabled">
    <legend>选择追溯袋码</legend>
    <p class="pick-summary" role="status">可选 {{eligible.length}} 袋 · 本次已选 <strong>{{modelValue.length}}</strong> 袋</p>
    <div class="pick-controls">
      <label>本次拣货数量<input v-model="desired" type="number" min="1" :max="eligible.length" step="1" placeholder="输入袋数" /></label>
      <button type="button" :disabled="!eligible.length" @click="chooseQuantity">按数量选择袋码</button>
      <button type="button" :disabled="!modelValue.length" @click="emit('update:modelValue', [])">清空选择</button>
    </div>
    <p class="pick-help">按数量选择将替换当前选择,按袋码顺序选取可选袋。请核对实物,或逐袋勾选。</p>
    <p v-if="selectionError" class="pick-error" role="alert">{{selectionError}}</p>
    <label class="pick-search">查找袋码<input v-model="search" type="search" placeholder="输入业务码或追溯码" /></label>
    <div class="pick-list">
      <label v-for="bag in filtered.slice(0, limit)" :key="bag.id" class="pick-option" :class="{ 'pick-unavailable': !bag.selectable, 'pick-selected': modelValue.includes(bag.id) }">
        <input type="checkbox" :checked="modelValue.includes(bag.id)" :disabled="!bag.selectable" :aria-label="'选择袋码 ' + bag.code" @change="choose(bag, ($event.target as HTMLInputElement).checked)" />
        <span><strong>{{bag.code}}</strong><small v-if="bag.businessCode">业务码 {{bag.businessCode}}</small><small v-if="bag.unavailableReason">{{bag.unavailableReason}}</small></span>
      </label>
      <p v-if="!filtered.length" class="pick-help">没有匹配的袋码</p>
    </div>
    <button v-if="filtered.length > limit" type="button" @click="limit += 50">显示更多袋码({{limit}} / {{filtered.length}})</button>
    <details v-if="modelValue.length"><summary>核对全部已选袋码({{modelValue.length}} 袋)</summary><p v-for="bag in bags.filter(b => modelValue.includes(b.id))" :key="bag.id" class="pick-code">{{bag.code}}<small v-if="bag.businessCode"> · {{bag.businessCode}}</small></p></details>
  </fieldset>
</template>
<style scoped>
.pick-bags{border:0;padding:0;margin:16px 0;min-width:0;color:inherit}.pick-bags legend{font-weight:600}.pick-summary{margin:8px 0}.pick-controls{display:flex;align-items:flex-end;gap:8px;flex-wrap:wrap}.pick-controls label{flex:1;min-width:130px}.pick-bags label{font-weight:500}.pick-bags input[type=number],.pick-bags input[type=search]{display:block;width:100%;min-width:0;min-height:44px;border:1px solid #64748b;border-radius:6px;background:var(--background, white);color:inherit;padding:8px 10px;font:inherit}.pick-bags button{min-height:44px;padding:8px 12px;border:1px solid #64748b;border-radius:6px;color:#0369a1;background:var(--background, white);cursor:pointer}.pick-bags button:disabled{opacity:.5;cursor:default}.pick-help{font-size:14px;color:#475569;line-height:1.5;margin:10px 0}.pick-search{display:block;margin:12px 0}.pick-list{max-height:360px;overflow:auto;border-top:1px solid #e2e8f0}.pick-option{display:flex;align-items:center;gap:12px;min-height:64px;padding:10px 4px;border-bottom:1px solid #e2e8f0;cursor:pointer}.pick-option input{width:20px;height:20px;flex-shrink:0;accent-color:#0369a1}.pick-option span{min-width:0;overflow-wrap:anywhere}.pick-option strong{display:block;font-weight:500}.pick-option small{display:block;color:#475569;font-size:14px}.pick-selected{background:#f0f9ff;color:#0f172a}.pick-unavailable{cursor:default;background:#f8fafc;color:#475569}.pick-error{color:#b91c1c}.pick-code{overflow-wrap:anywhere;font-size:14px;padding:6px 0}.pick-bags summary{cursor:pointer;min-height:44px;padding:10px 0;color:#0369a1}.pick-bags :focus-visible{outline:2px solid #0369a1;outline-offset:2px}
</style>