spring
5 小时以前 3ea1ff641e1c680a5a1727fb4034797bfe65d93e
src/pages/qualityManagement/nonconformingManagement/index.vue
@@ -2,74 +2,69 @@
  <view class="nonconforming-management-page">
    <PageHeader title="不合格品管理" @back="goBack" />
    
    <!-- 搜索与筛选 -->
    <!-- 搜索与筛选(样式参照仓储物流模块) -->
    <view class="search-section">
      <up-search
        placeholder="请输入产品名称搜索"
        v-model="searchForm.productName"
        @search="handleQuery"
        @custom="handleQuery"
        @clear="handleQuery"
        :show-action="true"
        action-text="搜索"
        :animation="true"
        customStyle="margin-bottom: 20rpx"
      ></up-search>
      <view class="search-row">
        <view class="search-input-wrap">
          <up-input v-model="searchForm.productName" placeholder="产品名称" clearable />
        </view>
        <view class="btn-search" @click="handleQuery">
          <view class="btn-search-inner">
            <up-icon name="search" size="22" color="#fff"></up-icon>
            <text>搜索</text>
          </view>
        </view>
      </view>
      <view class="filter-row">
        <view class="filter-item" @click="showTypeSelect = true">
          <text>{{ typeLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        </view>
        <view class="filter-item" @click="showStatusSelect = true">
          <text>{{ statusLabel }}</text>
          <up-icon name="arrow-down" size="14" color="#999"></up-icon>
        <view class="filter-item" @click="openDateRange">
          <text>{{ dateRangeLabel }}</text>
          <up-icon name="calendar" size="14" color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- 列表区域 -->
    <view class="list-container" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="list-item">
        <view class="item-header">
          <text class="product-name">{{ item.productName }}</text>
          <up-tag :text="getStatusText(item.inspectState)" :type="getStatusType(item.inspectState)" size="mini"></up-tag>
        </view>
        <view class="item-content">
          <view class="item-row">
            <text class="item-label">类别:</text>
            <text class="item-value">{{ getInspectTypeText(item.inspectType) }}</text>
    <view class="list-section">
      <view v-if="tableData.length > 0">
        <view v-for="(item, index) in tableData" :key="item.id || index" class="card-item">
          <view class="card-click" @click="openForm('edit', item)">
            <view class="card-header">
              <view class="header-main">
                <text class="product-name">{{ item.productName }}</text>
              </view>
              <view class="header-sub">
                <text class="sub-title">{{ item.model || '-' }}</text>
                <up-tag :text="getInspectTypeText(item.checkType ?? item.inspectType)" type="primary" size="mini" />
              </view>
            </view>
            <up-divider />
            <view class="card-body">
              <view class="row"><text class="l">检测日期</text><text class="r">{{ item.checkTime || '-' }}</text></view>
              <view class="row"><text class="l">批号</text><text class="r">{{ item.batchNo || '-' }}</text></view>
              <view class="row"><text class="l">检验员</text><text class="r">{{ item.checkName || '-' }}</text></view>
              <view class="row"><text class="l">不合格现象</text><text class="r text-error">{{ item.defectivePhenomena || '-' }}</text></view>
              <view class="row" v-if="item.inspectState == 1"><text class="l">处理结果</text><text class="r text-success">{{ getDealResultLabel(item.dealResult) || item.dealResult || '-' }}</text></view>
              <view class="row" v-if="item.inspectState == 1"><text class="l">处理人</text><text class="r">{{ item.dealName || '-' }}</text></view>
              <view class="row" v-if="item.inspectState == 1"><text class="l">处理日期</text><text class="r">{{ item.dealTime || '-' }}</text></view>
            </view>
          </view>
          <view class="item-row">
            <text class="item-label">检测日期:</text>
            <text class="item-value">{{ item.checkTime || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">规格型号:</text>
            <text class="item-value">{{ item.model || '-' }}</text>
          </view>
          <view class="item-row">
            <text class="item-label">不合格现象:</text>
            <text class="item-value text-error">{{ item.defectivePhenomena || '-' }}</text>
          </view>
          <view class="item-row" v-if="item.inspectState === 1">
            <text class="item-label">处理结果:</text>
            <text class="item-value text-success">{{ item.dealResult || '-' }}</text>
          <view class="card-actions">
            <view class="btn-link btn-link-primary" v-if="item.inspectState == 0" @click.stop="openDealDialog(item)">处理</view>
            <view class="btn-link btn-link-plain" v-if="item.inspectState == 0" @click.stop="openForm('edit', item)">编辑</view>
            <view class="btn-link btn-link-warn" @click.stop="handleDelete(item)">删除</view>
          </view>
        </view>
        <view class="item-actions">
          <up-button v-if="item.inspectState === 0" type="primary" size="mini" @click.stop="openDealDialog(item)">处理</up-button>
          <up-button type="error" size="mini" @click.stop="handleDelete(item)">删除</up-button>
        <view class="load-more-wrap">
          <u-loadmore :status="loadStatus" @loadmore="loadMore" />
        </view>
      </view>
      <view class="pagination-container">
        <up-loadmore :status="loadStatus" @loadmore="getList" />
      </view>
      <view v-else class="no-data">暂无数据</view>
    </view>
    
    <view v-else class="no-data">
      <up-empty mode="data" text="暂无数据"></up-empty>
    </view>
    <!-- 类型选择器 -->
    <up-action-sheet
      :actions="typeActions"
@@ -77,15 +72,6 @@
      @close="showTypeSelect = false"
      @select="selectType"
      title="请选择类别"
    ></up-action-sheet>
    <!-- 状态选择器 -->
    <up-action-sheet
      :actions="statusActions"
      :show="showStatusSelect"
      @close="showStatusSelect = false"
      @select="selectStatus"
      title="请选择状态"
    ></up-action-sheet>
    <!-- 处理弹窗 -->
@@ -97,10 +83,19 @@
        <up-form :model="dealForm" ref="dealFormRef" label-width="100" label-position="top">
          <view class="info-summary">
            <text class="summary-text">产品:{{ currentItem?.productName }}</text>
            <text class="summary-text">不合格现象:{{ currentItem?.defectivePhenomena }}</text>
            <text class="summary-text">检测日期:{{ currentItem?.checkTime || '-' }}</text>
          </view>
          <up-form-item label="不合格现象" prop="defectivePhenomena" required borderBottom>
            <up-textarea v-model="dealForm.defectivePhenomena" placeholder="请输入不合格现象" count border="surround" />
          </up-form-item>
          <up-form-item label="处理结果" prop="dealResult" required borderBottom>
            <up-textarea v-model="dealForm.dealResult" placeholder="请输入处理结果" count border="surround" />
            <view class="selector-trigger" @click="showDealResultSelect = true">
              <text class="selector-text" :class="{ placeholder: !dealResultLabel }">{{ dealResultLabel || '请选择处理结果' }}</text>
              <up-icon name="arrow-down" size="14" color="#999"></up-icon>
            </view>
          </up-form-item>
          <up-form-item label="处理人" prop="dealName" required borderBottom>
            <up-input v-model="dealForm.dealName" placeholder="请输入处理人" border="surround" />
          </up-form-item>
          <up-form-item label="处理日期" prop="dealTime" required borderBottom>
            <up-input
@@ -119,6 +114,15 @@
      </view>
    </up-popup>
    <!-- 处理结果选择器 -->
    <up-action-sheet
      :actions="dealResultActions"
      :show="showDealResultSelect"
      @close="showDealResultSelect = false"
      @select="selectDealResult"
      title="请选择处理结果"
    />
    <!-- 日期选择器 -->
    <up-datetime-picker
      :show="showDatePicker"
@@ -127,23 +131,49 @@
      @confirm="confirmDate"
      @cancel="showDatePicker = false"
    ></up-datetime-picker>
    <!-- 录入日期范围选择:开始 -->
    <up-datetime-picker
      :show="showEntryStartPicker"
      v-model="entryStartValue"
      mode="date"
      @confirm="confirmEntryStart"
      @cancel="showEntryStartPicker = false"
    />
    <!-- 录入日期范围选择:结束 -->
    <up-datetime-picker
      :show="showEntryEndPicker"
      v-model="entryEndValue"
      mode="date"
      @confirm="confirmEntryEnd"
      @cancel="showEntryEndPicker = false"
    />
    <!-- 右下角新增按钮 -->
    <view class="fab-button" @click="openForm('add')">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
  </view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { ref, reactive, computed } from 'vue';
import {
  qualityUnqualifiedListPage,
  qualityUnqualifiedDeal,
  qualityUnqualifiedDel
} from '@/api/qualityManagement/nonconformingManagement.js';
import { toast, showConfirm } from '@/utils/common';
import { useDict } from '@/utils/dict'
import dayjs from 'dayjs';
import PageHeader from '@/components/PageHeader.vue'
import { onReachBottom, onShow } from '@dcloudio/uni-app'
const searchForm = reactive({
  productName: '',
  inspectType: '',
  inspectState: ''
  checkType: '',
  entryDateStart: undefined,
  entryDateEnd: undefined
});
const tableData = ref([]);
@@ -162,54 +192,67 @@
  { name: '出厂检', value: '2' }
];
const typeLabel = computed(() => {
  const action = typeActions.find(a => a.value === searchForm.inspectType);
  const action = typeActions.find(a => a.value === String(searchForm.checkType ?? ''));
  return action ? action.name : '全部类别';
});
const showStatusSelect = ref(false);
const statusActions = [
  { name: '全部', value: '' },
  { name: '待处理', value: '0' },
  { name: '已处理', value: '1' }
];
const statusLabel = computed(() => {
  const action = statusActions.find(a => a.value === searchForm.inspectState);
  return action ? action.name : '全部状态';
});
const dateRangeLabel = computed(() => {
  if (searchForm.entryDateStart && searchForm.entryDateEnd) return `${searchForm.entryDateStart}~${searchForm.entryDateEnd}`
  if (searchForm.entryDateStart) return `${searchForm.entryDateStart}~`
  if (searchForm.entryDateEnd) return `~${searchForm.entryDateEnd}`
  return '检测日期'
})
const dealDialogVisible = ref(false);
const submitLoading = ref(false);
const currentItem = ref(null);
const dealForm = reactive({
  id: null,
  defectivePhenomena: '',
  dealResult: '',
  dealName: '',
  dealTime: dayjs().format('YYYY-MM-DD')
});
const { rejection_handling } = useDict('rejection_handling')
const showDealResultSelect = ref(false)
const dealResultActions = computed(() => {
  const list = rejection_handling?.value || []
  return (list || []).map(it => ({ name: it.label, value: it.value }))
})
const dealResultLabel = computed(() => {
  const list = rejection_handling?.value || []
  const v = dealForm.dealResult
  return (list || []).find(it => String(it.value) === String(v))?.label || ''
})
function getDealResultLabel(value) {
  const list = rejection_handling?.value || []
  return (list || []).find(it => String(it.value) === String(value))?.label || ''
}
const showDatePicker = ref(false);
const dateValue = ref(Number(new Date()));
const showEntryStartPicker = ref(false)
const showEntryEndPicker = ref(false)
const entryStartValue = ref(Date.now())
const entryEndValue = ref(Date.now())
const getInspectTypeText = (type) => {
  const types = { '0': '入厂检', '1': '车间检', '2': '出厂检' };
  return types[type] || '-';
};
const getStatusText = (state) => {
  return state === 1 ? '已处理' : '待处理';
};
const getStatusType = (state) => {
  return state === 1 ? 'success' : 'warning';
  return types[String(type ?? '')] || '-';
};
const getList = () => {
  if (loadStatus.value === 'loading' || (page.total > 0 && tableData.value.length >= page.total)) return;
  loadStatus.value = 'loading';
  const isFirstPage = page.current === 1
  if (loadStatus.value === 'loading' || (!isFirstPage && page.total > 0 && tableData.value.length >= page.total)) return
  loadStatus.value = 'loading'
  const params = {
    productName: searchForm.productName || null,
    inspectType: searchForm.inspectType || null,
    inspectState: searchForm.inspectState || null,
    checkType: searchForm.checkType === '' ? null : searchForm.checkType,
    entryDateStart: searchForm.entryDateStart,
    entryDateEnd: searchForm.entryDateEnd,
    current: page.current,
    size: page.size
  };
@@ -227,12 +270,18 @@
      loadStatus.value = 'nomore';
    } else {
      loadStatus.value = 'loadmore';
      page.current++;
    }
  }).catch(() => {
    loadStatus.value = 'loadmore';
    loadStatus.value = 'error';
  });
};
const loadMore = () => {
  if (loadStatus.value === 'nomore' || loadStatus.value === 'loading') return
  loadStatus.value = 'loading'
  page.current++
  getList()
}
const handleQuery = () => {
  page.current = 1;
@@ -243,27 +292,55 @@
};
const selectType = (e) => {
  searchForm.inspectType = e.value;
  searchForm.checkType = e.value;
  handleQuery();
};
const selectStatus = (e) => {
  searchForm.inspectState = e.value;
  handleQuery();
};
const openDateRange = () => {
  entryStartValue.value = searchForm.entryDateStart ? dayjs(searchForm.entryDateStart, 'YYYY-MM-DD').valueOf() : Date.now()
  showEntryStartPicker.value = true
}
const confirmEntryStart = (e) => {
  const ts = e?.value ?? entryStartValue.value
  searchForm.entryDateStart = dayjs(ts).format('YYYY-MM-DD')
  showEntryStartPicker.value = false
  entryEndValue.value = searchForm.entryDateEnd ? dayjs(searchForm.entryDateEnd, 'YYYY-MM-DD').valueOf() : Date.now()
  showEntryEndPicker.value = true
}
const confirmEntryEnd = (e) => {
  const ts = e?.value ?? entryEndValue.value
  searchForm.entryDateEnd = dayjs(ts).format('YYYY-MM-DD')
  showEntryEndPicker.value = false
  handleQuery()
}
const openDealDialog = (item) => {
  currentItem.value = item;
  dealForm.id = item.id;
  dealForm.dealResult = '';
  dealForm.defectivePhenomena = item.defectivePhenomena || ''
  dealForm.dealResult = item.dealResult || '';
  dealForm.dealName = item.dealName || ''
  dealForm.dealTime = dayjs().format('YYYY-MM-DD');
  dealDialogVisible.value = true;
};
const selectDealResult = (e) => {
  dealForm.dealResult = e.value
  showDealResultSelect.value = false
}
const submitDeal = () => {
  if (!dealForm.dealResult) {
    toast('请输入处理结果');
  if (!dealForm.defectivePhenomena) {
    toast('请输入不合格现象')
    return;
  }
  if (!dealForm.dealResult) {
    toast('请选择处理结果')
    return
  }
  if (!dealForm.dealName) {
    toast('请输入处理人')
    return
  }
  submitLoading.value = true;
  qualityUnqualifiedDeal(dealForm).then(() => {
@@ -291,89 +368,115 @@
  showDatePicker.value = false;
};
const openForm = (type, row) => {
  if (type !== 'add' && row?.inspectState == 1) {
    toast('已处理的数据不能再编辑')
    return
  }
  const id = row?.id
  uni.navigateTo({
    url: `/pages/qualityManagement/nonconformingManagement/form?type=${type}${id ? `&id=${id}` : ''}`
  })
}
const goBack = () => {
  uni.navigateBack();
};
onMounted(() => {
  handleQuery();
});
onShow(() => {
  handleQuery()
})
onReachBottom(() => {
  loadMore()
})
</script>
<style lang="scss" scoped>
.nonconforming-management-page {
  padding-bottom: 20rpx;
  background-color: #f5f7fa;
  min-height: 100vh;
  background: #f5f5f5;
  padding-bottom: 120rpx;
}
.search-section {
  padding: 20rpx 30rpx;
  background-color: #ffffff;
  position: sticky;
  top: 0;
  z-index: 10;
  background: #fff;
  margin: 24rpx;
  padding: 24rpx;
  border-radius: 16rpx;
}
.search-row { display: flex; align-items: center; margin-bottom: 20rpx; }
.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
.btn-search {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 180rpx;
  min-height: 72rpx;
  flex-shrink: 0;
  padding: 20rpx 24rpx;
  background: #2979ff;
  color: #fff;
  border-radius: 12rpx;
  font-size: 28rpx;
  box-sizing: border-box;
  text-align: center;
}
.btn-search-inner { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8rpx; }
.filter-row {
  display: flex;
  justify-content: space-around;
  padding: 10rpx 0;
  gap: 20rpx;
}
.filter-item {
  display: flex;
  align-items: center;
  gap: 10rpx;
  font-size: 28rpx;
  color: #606266;
}
.list-container {
  padding: 20rpx;
}
.list-item {
  background-color: #ffffff;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.item-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.product-name {
  font-size: 30rpx;
  font-weight: bold;
  color: #303133;
}
.item-content {
  margin-bottom: 20rpx;
}
.item-row {
  display: flex;
  margin-bottom: 10rpx;
}
.item-label {
  color: #909399;
  width: 180rpx;
  font-size: 28rpx;
}
.item-value {
  flex: 1;
  color: #303133;
  font-size: 28rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18rpx 20rpx;
  background: #f5f5f5;
  border-radius: 12rpx;
  font-size: 26rpx;
  color: #666;
}
.list-section { padding: 0 24rpx; }
.card-item {
  background: #fff;
  border-radius: 16rpx;
  padding: 16rpx 20rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.card-header { padding: 2rpx 0 6rpx; }
.header-main { display: flex; justify-content: space-between; align-items: center; gap: 16rpx; }
.product-name { font-size: 30rpx; font-weight: 500; color: #333; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.header-sub { display: flex; justify-content: space-between; gap: 16rpx; margin-top: 6rpx; }
.sub-title { font-size: 24rpx; color: #999; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sub-right { font-size: 24rpx; color: #999; flex-shrink: 0; }
.card-body .row { display: flex; justify-content: space-between; padding: 6rpx 0; font-size: 26rpx; }
.card-body .l { color: #666; width: 180rpx; flex-shrink: 0; }
.card-body .r { color: #333; flex: 1; text-align: right; word-break: break-all; }
.card-actions {
  display: flex;
  gap: 16rpx;
  justify-content: flex-end;
  align-items: center;
  margin-top: 12rpx;
  padding-top: 14rpx;
  border-top: 1rpx solid #eee;
}
.btn-link {
  font-size: 28rpx;
  padding: 10rpx 22rpx;
  border-radius: 999rpx;
  border: 1rpx solid transparent;
}
.btn-link-primary { color: #2979ff; border-color: rgba(41, 121, 255, 0.4); background: rgba(41, 121, 255, 0.08); }
.btn-link-plain { color: #606266; border-color: rgba(96, 98, 102, 0.35); background: rgba(96, 98, 102, 0.06); }
.btn-link-warn { color: #f56c6c; border-color: rgba(245, 108, 108, 0.55); background: rgba(245, 108, 108, 0.08); }
.text-error {
  color: #f56c6c;
@@ -383,17 +486,8 @@
  color: #67c23a;
}
.item-actions {
  display: flex;
  justify-content: flex-end;
  gap: 20rpx;
  border-top: 1rpx solid #ebeef5;
  padding-top: 20rpx;
}
.no-data {
  padding-top: 200rpx;
}
.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
.load-more-wrap { padding: 24rpx 24rpx 8rpx; }
.dialog-content {
  width: 650rpx;
@@ -431,4 +525,23 @@
.dialog-footer {
  margin-top: 40rpx;
}
.selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; }
.selector-text { font-size: 28rpx; color: #333; }
.selector-text.placeholder { color: #999; }
.fab-button {
  position: fixed;
  right: 36rpx;
  bottom: 72rpx;
  width: 104rpx;
  height: 104rpx;
  border-radius: 52rpx;
  background: #2979ff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 10rpx 26rpx rgba(41, 121, 255, 0.35);
  z-index: 20;
}
</style>