index.vue 8.95 KB
<template>
  <uni-popup ref="popup" type="bottom" :mask-click="false" :safe-area="true">
    <view class="relate-sheet">
      <view class="sheet-header">
        <text class="cancel" @click="onCancel">取消</text>
        <text class="title">{{ title }}</text>
        <text class="ok" @click="onConfirm">确认{{ confirmCountText }}</text>
      </view>
      <view class="sheet-search">
        <uni-search-bar v-model="searchKeyword" @input="onSearchInput" @confirm="onConfirmSearch" :placeholder="placeholder" />
      </view>
      <view class="sheet-body">
        <CardList
          ref="cardRef"
          :fetchFn="fetchList"
          :pageSize="pageSize"
          :query="listQuery"
          :extra="listExtra"
          :selectable="true"
          :row-key="rowKey"
          :selectedKeys.sync="innerSelectedKeys"
          :show-check="false"
          @loaded="onLoaded"
        >
          <template v-slot="{ item, selected }">
            <view :class="['card', { selected }]">
              <view v-for="(f,i) in displayFields" :key="i" class="row">
                <text class="label">{{ f.label }}</text>
                <text class="value">{{ formatValue(item, f) }}</text>
              </view>
            </view>
          </template>
        </CardList>
      </view>
    </view>
  </uni-popup>
</template>

<script>
import CardList from '@/components/card/index.vue'
import { customerQueryApi } from '@/api/devManage.js'
import { userSelector } from '@/api/system/user.js'
import { listCanRevokeOrChangeOrderInfo } from '@/api/order_list.js'

export default {
  name: 'RelateSelectSheet',
  components: { CardList },
  props: {
    visible: { type: Boolean, default: false },
    title: { type: String, default: '选择' },
    // 传入显示字段配置:[{ label, field }]
    displayFields: { type: Array, default: () => [ { label: '名称', field: 'name' } ] },
    // 如果未提供 fetchFn,可通过 source 指定内置数据源:'customer' | 'user'
    source: { type: String, default: '' },
    fetchFn: { type: Function, default: null },
    // 为内置数据源传递额外参数,如 { queryType: 'CHANGE' }
    sourceExtra: { type: Object, default: () => ({}) },
    pageSize: { type: Number, default: 10 },
    multiple: { type: Boolean, default: false },
    rowKey: { type: String, default: 'id' },
    // 选中回显:由父组件传入,支持 .sync / v-model:selectedKeys
    selectedKeys: { type: Array, default: () => [] }
  },
  data() {
    return {
      innerSelectedKeys: this.selectedKeys.slice(0),
      currentItems: [],
      searchKeyword: '',
      searchKeywordDebounced: '',
      searchDebounceTimer: null,
      placeholder: '搜索',
      // 使用稳定对象,避免因重新渲染创建新对象导致列表 reload
      listQuery: {},
      listExtra: { keyword: '', name: '' }
    }
  },
  computed: {
    confirmCountText() {
      const n = Array.isArray(this.innerSelectedKeys) ? this.innerSelectedKeys.length : 0
      return n > 0 ? `(${n})` : ''
    }
  },
  watch: {
    visible(v) { v ? this.open() : this.close() },
    selectedKeys(keys) {
      // 父传入选中回显,只同步到内部,不向父回推,避免循环
      const incoming = Array.isArray(keys) ? keys : []
      if (!this.multiple && incoming.length > 1) {
        const last = incoming[incoming.length - 1]
        this.innerSelectedKeys = [last]
      } else {
        this.innerSelectedKeys = incoming.slice(0)
      }
    },
    innerSelectedKeys(keys) {
      // 内部选择变化时,保证单选只保留最后一个
      const arr = Array.isArray(keys) ? keys : []
      if (!this.multiple && arr.length > 1) {
        const last = arr[arr.length - 1]
        // 仅当确实需要收敛时再赋值,减少不必要的触发
        if (!(arr.length === 1 && arr[0] === last)) {
          this.innerSelectedKeys = [last]
        }
      }
    }
  },
  mounted() {
    if (this.visible) this.open()
  },
  methods: {
    open() {
      this.$refs.popup && this.$refs.popup.open()
      this.$emit('update:visible', true)
      // 合并外部传入的额外参数
      this.listExtra = { ...this.listExtra, ...this.sourceExtra }
    },
    close() {
      this.$refs.popup && this.$refs.popup.close()
      this.$emit('update:visible', false)
    },
    fetchList({ pageIndex, pageSize, query, extra }) {
      const name = (extra && (extra.name || extra.keyword || extra.key)) || ''
      if (typeof this.fetchFn === 'function') {
        return this.fetchFn({ pageIndex, pageSize, query, extra })
      }
      // 内置数据源
      try {
        // 客户池
        if (this.source === 'customer') {
          console.log('customer_extra', extra)
          const source = (extra && extra.source) || '';
          const params = { pageIndex, pageSize, name, source }
          return customerQueryApi(params).then(res => {
            const _data = res.data || {}
            const records = _data.datas || []
            const totalCount = _data.totalCount || 0
            const hasNext = _data.hasNext || false
            return { records, totalCount, hasNext }
          })
        } else if (this.source === 'user') {
          // 人员表
          const params = { pageIndex, pageSize, name, username: name }
          return userSelector(params).then(res => {
            const _data = res.data || {}
            const records = _data.datas || _data.records || _data.list || []
            const totalCount = _data.totalCount || _data.count || 0
            const hasNext = _data.hasNext || false
            return { records, totalCount, hasNext }
          })
        } else if (this.source === 'orderAssoc') {
          console.log('orderAssoc_extra', extra)
          // 订单关联表(可撤销/变更的订货单)
          const queryType = (extra && extra.queryType) || ''
          const params = { pageIndex, pageSize, orderNo: name, queryType }
          return listCanRevokeOrChangeOrderInfo(params).then(res => {
            const _data = res.data || {}
            const records = _data.datas || _data.records || _data.list || []
            const totalCount = _data.totalCount || _data.count || 0
            const hasNext = _data.hasNext || false
            return { records, totalCount, hasNext }
          })
        }
      } catch (e) {
        return Promise.resolve({ records: [], totalCount: 0, hasNext: false })
      }
      return Promise.resolve({ records: [], totalCount: 0, hasNext: false })
    },
    formatValue(item, f) {
      const v = f && f.field ? item[f.field] : ''
      if (f && typeof f.format === 'function') {
        const r = f.format(v, item)
        return r == null ? '' : String(r)
      }
      if (f && f.map && typeof f.map === 'object' && f.map !== null) {
        if (Object.prototype.hasOwnProperty.call(f.map, v)) {
          const r = f.map[v]
          return r == null ? '' : String(r)
        }
      }
      return v == null ? '' : String(v)
    },
    onLoaded({ items }) {
      this.currentItems = items || []
    },
    onSearchInput(val) {
      this.searchKeyword = val
      clearTimeout(this.searchDebounceTimer)
      this.searchDebounceTimer = setTimeout(() => {
        this.searchKeywordDebounced = this.searchKeyword
        if (this.source === 'customer' || this.source === 'user') {
          this.listExtra.name = this.searchKeyword
        } else {
          this.listExtra.keyword = this.searchKeyword
        }
      }, 300)
    },
    onConfirmSearch() {
      this.searchKeywordDebounced = this.searchKeyword
      if (this.source === 'customer' || this.source === 'user') {
        this.listExtra.name = this.searchKeyword
      } else {
        this.listExtra.keyword = this.searchKeyword
      }
    },
    onCancel() { this.close() },
    onConfirm() {
      const rowKey = this.rowKey || 'id'
      const selected = (this.currentItems || []).filter(it => this.innerSelectedKeys.includes(it[rowKey]))
      this.$emit('confirm', { items: selected })
      this.close()
    }
  }
}
</script>

<style lang="scss" scoped>
.relate-sheet {
  width: 100%;
  height: 80vh;
  background: #fff;
  border-radius: 20rpx 20rpx 0 0;
  display: flex;
  flex-direction: column;
}
.sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 30rpx 32rpx;
  border-bottom: 1rpx solid #f0f0f0;
}
.title { font-size: 36rpx; font-weight: 600; }
.cancel { color: rgba(0,0,0,0.6); font-size: 28rpx; }
.ok { color: $theme-primary; font-size: 28rpx; }
.sheet-search { padding: 16rpx 24rpx; }
.sheet-body {
  flex: 1 1 auto; overflow-y: auto; padding: 24rpx;
  background: #f3f3f3;
}
.card { background: #fff; }
.card.selected {
  background-color: #fff;
  position: relative;
  &::after {
    content: '';
    position: absolute;
    top: -32rpx;
    left: -32rpx;
    bottom: -32rpx;
    right: -12rpx;
    border: 1px solid $theme-primary-alpha-50;
  }
}
.row { display: flex; gap: 16rpx; margin-bottom: 12rpx; &:last-child { margin-bottom: 0; } }
.label { color: rgba(0,0,0,0.6); width: 150rpx; }
.value { color: rgba(0,0,0,0.9); flex: 1; }
</style>