index.vue 10 KB
<template>
  <view class="page">
    <view class="framework-list-fixed">
      <view class="search-row">
        <uni-search-bar v-model="searchKeyword" radius="6" placeholder="请输入客户名称或编号" clearButton="auto"
          cancelButton="none" bgColor="#F3F3F3" textColor="rgba(0,0,0,0.4)" @confirm="search" @input="onSearchInput" />
        <view class="tool-icons">
          <image v-if="$auth.hasPermi('contract-manage:contract-framework:add')" class="tool-icon" src="/static/images/dev_manage/add_icon.png" @click="onAdd" />
          <image class="tool-icon" src="/static/images/dev_manage/filter_icon.png" @click="openFilter" />
        </view>
      </view>

    </view>
    <!-- 列表卡片组件 -->
    <view :class="['list-box', { 'pad-batch': batchMode }]">
      <card-list ref="cardRef" :fetch-fn="fetchList" :query="query" :extra="extraParams" :selectable="batchMode"
        row-key="id" :enable-refresh="true" :enable-load-more="true" @error="onCardError">
        <template v-slot="{ item, selected }">
          <view class="card" @click="goDetail(item)">
            <view class="card-header">
              <text class="title omit2">{{ item.customerName }}</text>
            </view>
            <view class="info-row">
              <text>框架合同编号</text><text>{{ item.code }}</text>
            </view>
            <view class="info-row">
              <text>是否签订</text><text>{{ item.hasFrameworkAgreement ? '是' : '否' }}</text>
            </view>
            <view class="info-row">
              <text>品种</text><text>{{ item.materialTypeName }}</text>
            </view>
            <view class="info-row">
              <text>授信截至时间</text><text>{{ item.validityTime }}</text>
            </view>
          </view>
        </template>
      </card-list>
    </view>
    <!-- 筛选弹框 -->
    <filter-modal :visible.sync="filterVisible" :value.sync="filterForm" title="筛选" @reset="onFilterReset"
      @confirm="onFilterConfirm">
      <template v-slot="{ model }">
        <view class="filter-form">
          <view class="form-item">
            <view class="label">品种</view>
            <uni-data-checkbox mode="tag" :multiple="false" :value-field="'value'" :text-field="'text'"
              v-model="model.materialTypeId" @change="onMaterialTypeChange" :localdata="materialTypeOptions" />
          </view>
          <view class="form-item">
            <view class="label">授权截止时间</view>
            <uni-datetime-picker type="daterange" v-model="model.dateRange" start="2023-01-01" />
          </view>
        </view>
      </template>
    </filter-modal>
  </view>
</template>

<script>
import CardList from '@/components/card/index.vue'
import FilterModal from '@/components/filter/index.vue'
import { getDicByCodes, getDicName } from '@/utils/dic';
import { queryApi } from '@/api/contract.js'
import { productVarietyQueryApi } from '@/api/devManage.js'
export default {
  components: { CardList, FilterModal },
  data() {
    return {
      searchKeyword: '',
      searchKeywordDebounced: '',
      // 批量选择
      batchMode: false,
      // 给到 card 的筛选值
      query: { materialTypeId: '', dateRange: [] },
      extraParams: {},

      // 筛选弹框
      filterVisible: false,
      filterForm: { materialTypeId: '', dateRange: [] },
      materialTypeOptions: [],
    }

  },
  computed: {

  },
  watch: {

  },
  created() {
    this.getProductVariety();
  },
  // 页面触底兜底:当页面自身滚动到底部时,转调卡片组件加载更多
  onReachBottom() {
    if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.onLoadMore) {
      this.$refs.cardRef.onLoadMore()
    }
  },
  beforeDestroy() {
    if (this.searchDebounceTimer) {
      clearTimeout(this.searchDebounceTimer)
      this.searchDebounceTimer = null
    }
  },
  methods: {
    // uni-search-bar 确认搜索:更新关键字并触发 CardList 刷新
    search(e) {
      const val = e && e.value != null ? e.value : this.searchKeyword
      this.searchKeyword = val
      this.searchKeywordDebounced = val
    },
    // 输入实时搜索:1200ms 防抖,仅在停止输入超过阈值后刷新
    onSearchInput(val) {
      if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer)
      this.searchDebounceTimer = setTimeout(() => {
        this.searchKeywordDebounced = this.searchKeyword
        this.searchDebounceTimer = null
      }, 1200)
    },
    // 列表接口(真实请求)
    fetchList({ pageIndex, pageSize, query, extra }) {
      const params = { pageIndex, pageSize, ...extra, ...query }
      // 处理日期范围
      if (Array.isArray(params.dateRange) && params.dateRange.length === 2) {
        params.validityTimeStart = params.dateRange[0] + ' 00:00:00'
        params.validityTimeEnd = params.dateRange[1] + ' 23:59:59'
        delete params.dateRange
      }
      // 关键字(使用去抖后的值避免频繁触发)
      if (this.searchKeywordDebounced) {
        params.customerName = this.searchKeywordDebounced
      }
      return queryApi(params)
        .then(res => {
          console.log('fetchList', res)
          const _data = res.data || {};
          const records = _data.datas || [];
          const totalCount = _data.totalCount || 0;
          const hasNext = _data.hasNext || false
          return { records, totalCount, hasNext }
        })
        .catch(err => {
          console.error('fetchList error', err)
          this.onCardError()
          return { records: [], totalCount: 0, hasNext: false }
        })
    },
    onCardError() {
      uni.showToast({ title: '列表加载失败', icon: 'none' })
    },

    openFilter() {
      this.filterVisible = true
    },
    onFilterReset(payload) {
      // 保持弹框不关闭,仅同步表单
      this.filterForm = payload
    },
    onFilterConfirm(payload) {
      // 合并筛选值
      this.query = { ...this.query, ...payload }
    },
    onMaterialTypeChange(e) {
      const raw = e && e.detail && e.detail.value !== undefined
        ? e.detail.value
        : (e && e.value !== undefined ? e.value : '')
      // 直接同步到外层 filterForm,驱动 FilterModal 的 innerModel 更新
      this.filterForm.materialTypeId = raw
    },

    onAdd() {
      uni.navigateTo({ url: '/pages/contract_framework/add' })
    },

    goDetail(item) {
      const id = item && item.id ? item.id : ''
      if (!id) return
      uni.navigateTo({ url: `/pages/contract_framework/detail?id=${id}` })
    },



    getProductVariety() {
      productVarietyQueryApi({
        pageIndex: 1,
        pageSize: 9999,
        available: true
      }).then(res => {
        const _data = res.data || {};
        const records = _data.datas || [];
        this.materialTypeOptions = records.map(item => ({
          value: item.id,
          text: item.name
        }))
      })
    },
  }
}
</script>

<style lang="scss" scoped>
.page {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.framework-list-fixed {
  position: fixed;
  top: 96rpx;
  left: 0;
  right: 0;
  z-index: 2;
  background: #fff;

  .search-row {
    display: flex;
    align-items: center;
    padding: 16rpx 32rpx;

    .uni-searchbar {
      padding: 0;
      flex: 1;
    }

    .tool-icons {
      display: flex;

      .tool-icon {
        width: 48rpx;
        height: 48rpx;
        display: block;
        margin-left: 32rpx;
      }
    }
  }


}

/* 仅当前页覆盖 uni-search-bar 盒子高度 */
::v-deep .uni-searchbar__box {
  height: 80rpx !important;
  justify-content: start;

  .uni-searchbar__box-search-input {
    font-size: 32rpx !important;
  }
}

.list-box {
  flex: 1;
  padding-top: 140rpx;

  &.pad-batch {
    padding-bottom: 144rpx;
  }

  .card {
    position: relative;
  }

  .card-header {
    margin-bottom: 28rpx;
    position: relative;

    .title {
      font-size: 36rpx;
      font-weight: 600;
      line-height: 50rpx;
      color: rgba(0, 0, 0, 0.9);
      width: 578rpx;
    }

    .status {
      font-size: 30rpx;
      font-weight: 600;
      position: absolute;
      top: -36rpx;
      right: -12rpx;
      height: 48rpx;
      line-height: 48rpx;
      color: #fff;
      font-size: 24rpx;
      padding: 0 14rpx;
      border-radius: 6rpx;

      &.status_1 {
        background: #3D48A3;
      }

      &.status_2 {
        background: #2BA471;
      }

      &.status_3 {
        background: #D54941;
      }

      &.status_4 {
        background: #E7E7E7;
        color: rgba(0, 0, 0, 0.9);
      }
    }
  }

  .info-row {
    display: flex;
    align-items: center;
    color: rgba(0, 0, 0, 0.6);
    font-size: 28rpx;
    margin-bottom: 24rpx;
    height: 32rpx;

    &:last-child {
      margin-bottom: 0;
    }

    text {
      width: 60%;

      &:last-child {
        color: rgba(0, 0, 0, 0.9);
        width: 40%;
      }
    }
  }
}

.filter-form {
  .form-item {
    margin-bottom: 24rpx;
  }

  .label {
    margin-bottom: 20rpx;
    color: rgba(0, 0, 0, 0.9);
    height: 44rpx;
    line-height: 44rpx;
    font-size: 30rpx;
  }

  .fake-select {
    height: 80rpx;
    line-height: 80rpx;
    padding: 0 20rpx;
    background: #f3f3f3;
    border-radius: 12rpx;

    .placeholder {
      color: #999;
    }

    .value {
      color: #333;
    }
  }
}

/* 深度覆盖 uni-data-checkbox(mode=tag)内部的 tag 展示与间距 */
::v-deep .filter-form .uni-data-checklist .checklist-group {
  .checklist-box {
    &.is--tag {
      width: 212rpx;
      margin-top: 0;
      margin-bottom: 24rpx;
      margin-right: 24rpx;
      height: 80rpx;
      padding: 0;
      border-radius: 12rpx;
      background-color: #f3f3f3;
      border-color: #f3f3f3;

      &:nth-child(3n) {
        margin-right: 0;
      }

      .checklist-content {
        display: flex;
        justify-content: center;
      }

      .checklist-text {
        color: rgba(0, 0, 0, 0.9);
        font-size: 28rpx;
      }
    }

    &.is-checked {
      background-color: $theme-primary-plain-bg !important;
      border-color: $theme-primary-plain-bg !important;

      .checklist-text {
        color: $theme-primary !important;
      }
    }
  }

}
</style>