spring
5 小时以前 cc3001ab9e0ab8ce673b812a22ebea1edd332f2a
src/pages/inventoryManagement/receiptManagement/index.vue
@@ -1,406 +1,358 @@
<template>
  <view class="stock-in-page">
    <PageHeader title="自定义入库" @back="goBack" />
  <view class="receipt-page">
    <PageHeader title="入库管理" @back="goBack" />
    <!-- 标签:合格入库 / 不合格入库 -->
    <view class="tabs-wrap">
      <view
        v-for="tab in tabs"
        :key="tab.name"
        class="tab-item"
        :class="{ active: activeTab === tab.name }"
        @click="activeTab = tab.name"
      >
        <text>{{ tab.label }}</text>
      </view>
    </view>
    <!-- 搜索区域 -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
      <view class="search-row">
        <view class="search-input-wrap">
          <up-input
            v-model="searchForm.supplierName"
            placeholder="请输入供应商名称"
            v-model="searchForm.productName"
            placeholder="产品大类"
            clearable
          />
        </view>
        <view class="search-button" @click="handleQuery">
          <up-icon name="search" size="24" color="#999"></up-icon>
        <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="date-filter" @click="openDatePickerHandler">
        <text class="date-text">{{ searchForm.timeStr || '选择日期' }}</text>
        <up-icon name="calendar" size="18" color="#999"></up-icon>
      </view>
    </view>
    <!-- 列表 -->
    <view class="stock-list" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="index" class="stock-item">
        <view class="item-header">
          <view class="item-left">
            <view class="batch-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
    <!-- 列表(合格/不合格共用接口 type 区分) -->
    <view class="list-section" v-if="activeTab !== 'custom'">
      <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="goDetail(item)">
            <view class="card-header">
              <view class="header-main">
                <text class="product-name">{{ item.productName }}</text>
                <text class="inbound-date">{{ item.createTime || item.inboundDate }}</text>
              </view>
            </view>
            <text class="batch-text">{{ item.inboundBatches }}</text>
            <up-divider />
            <view class="card-body">
              <view class="row"><text class="l">规格型号</text><text class="r">{{ item.model }}</text></view>
              <view class="row"><text class="l">单位</text><text class="r">{{ item.unit }}</text></view>
              <view class="row"><text class="l">入库数量</text><text class="r highlight">{{ item.stockInNum }}</text></view>
              <view class="row"><text class="l">入库人</text><text class="r">{{ item.createBy }}</text></view>
              <view class="row" v-if="item.recordType !== undefined"><text class="l">来源</text><text class="r">{{ getRecordType(item.recordType) || item.recordType }}</text></view>
            </view>
          </view>
          <view class="item-right">
            <text class="time-text">{{ item.inboundDate }}</text>
          <view class="card-actions">
            <view class="btn-delete" @click.stop="handleDeleteSingle(item)">删除</view>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row">
            <text class="detail-label">供应商名称</text>
            <text class="detail-value">{{ item.supplierName }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">产品大类</text>
            <text class="detail-value">{{ item.productCategory }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">规格型号</text>
            <text class="detail-value">{{ item.specificationModel }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">物品类型</text>
            <text class="detail-value">{{ item.itemType }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">入库数量</text>
            <text class="detail-value highlight">{{ item.inboundNum }} {{ item.unit }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">含税单价</text>
            <text class="detail-value">¥{{ item.taxInclusiveUnitPrice }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">含税总价</text>
            <text class="detail-value price">¥{{ item.taxInclusiveTotalPrice }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">税率</text>
            <text class="detail-value">{{ item.taxRate }}%</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">入库人</text>
            <text class="detail-value">{{ item.createBy }}</text>
          </view>
        </view>
        <view class="item-actions">
          <u-button type="primary" size="small" @click="handleEdit(item)">编辑</u-button>
          <u-button type="error" size="small" plain @click="handleDeleteSingle(item)">删除</u-button>
        </view>
      </view>
      <view v-else class="no-data">暂无数据</view>
    </view>
    <view v-else class="no-data">
      <text>暂无数据</text>
    <!-- 加载更多 -->
    <view class="load-more-wrap" v-if="tableData.length > 0">
      <u-loadmore :status="loadStatus" @loadmore="loadMore" />
    </view>
    <!-- 浮动操作按钮 -->
    <view class="fab-button" @click="handleAdd">
      <up-icon name="plus" size="24" color="#ffffff"></up-icon>
    </view>
    <!-- 日期选择器 -->
    <up-popup :show="showDatePicker" mode="bottom" @close="showDatePicker = false">
      <up-datetime-picker
        :show="true"
        v-model="dateValue"
        @confirm="onDateConfirm"
        @cancel="showDatePicker = false"
        mode="date"
      />
    </up-popup>
    <!-- 表单弹窗 -->
    <form-dia-manual ref="formDiaManual" @close="getList" @success="getList" />
  </view>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import dayjs from 'dayjs'
import { ref, reactive, toRefs, watch } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import FormDiaManual from './components/formDiaManual.vue'
import useUserStore from '@/store/modules/user'
import { formatDateToYMD } from '@/utils/ruoyi'
import {
  getInPageByCustom,
  delStockInCustom
} from "@/api/inventoryManagement/stockIn.js"
  getStockInRecordListPage,
  batchDeleteStockInRecords
} from '@/api/inventoryManagement/stockInRecord.js'
import {
  findAllQualifiedStockInRecordTypeOptions,
  findAllUnQualifiedStockInRecordTypeOptions
} from '@/api/basicData/enum.js'
const userStore = useUserStore()
const activeTab = ref('qualified')
const stockRecordTypeOptions = ref([])
const tabs = [
  { label: '合格入库', name: 'qualified', type: '0' },
  { label: '不合格入库', name: 'unqualified', type: '1' }
]
const tableData = ref([])
const showDatePicker = ref(false)
const dateValue = ref(new Date().getTime())
const formDiaManual = ref(null)
const page = reactive({
  current: 1,
  size: 20,
})
const total = ref(0)
const loadStatus = ref('loadmore') // loadmore | loading | nomore | error
const page = reactive({ current: 1, size: 4 })
const data = reactive({
  searchForm: {
    supplierName: '',
    timeStr: '',
  },
    productName: ''
  }
})
const { searchForm } = toRefs(data)
// 统一用 dayjs 输出 YYYY-MM-DD
const formatYMDLocal = (ts) => dayjs(Number(ts)).format('YYYY-MM-DD')
const currentType = () => tabs.find(t => t.name === activeTab.value)?.type || '0'
// 返回上一页
const goBack = () => {
  uni.navigateBack()
function getRecordType(recordType) {
  if (recordType == null || recordType === '') return ''
  return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || ''
}
// 查询列表
const handleQuery = () => {
  page.current = 1
  getList()
function fetchRecordTypeOptions() {
  const api = currentType() === '1'
    ? findAllUnQualifiedStockInRecordTypeOptions
    : findAllQualifiedStockInRecordTypeOptions
  api()
    .then(res => {
      const data = res.data != null ? res.data : res
      stockRecordTypeOptions.value = Array.isArray(data) ? data : []
    })
    .catch(() => {
      stockRecordTypeOptions.value = []
    })
}
const getList = () => {
  uni.showLoading({
    title: '加载中...',
    mask: true
  })
  if (activeTab.value === 'custom') return
  const isFirstPage = page.current === 1
  if (isFirstPage) {
    uni.showLoading({ title: '加载中...', mask: true })
  }
  const params = {
    ...page,
    supplierName: searchForm.value.supplierName,
    timeStr: searchForm.value.timeStr
    type: currentType(),
    productName: searchForm.value.productName
  }
  getInPageByCustom(params).then(res => {
    uni.hideLoading()
    tableData.value = res.data.records || []
    total.value = res.data.total || 0
  }).catch(() => {
    uni.hideLoading()
    uni.showToast({
      title: '加载失败',
      icon: 'none'
  getStockInRecordListPage(params)
    .then(res => {
      uni.hideLoading()
      const records = res.data?.records || []
      const totalCount = res.data?.total || 0
      if (isFirstPage) {
        tableData.value = records
        fetchRecordTypeOptions()
      } else {
        tableData.value = [...tableData.value, ...records]
      }
      total.value = totalCount
      if (tableData.value.length >= totalCount || totalCount === 0) {
        loadStatus.value = 'nomore'
      } else {
        loadStatus.value = 'loadmore'
      }
    })
    .catch(() => {
      uni.hideLoading()
      loadStatus.value = 'error'
      if (isFirstPage) {
        uni.showToast({ title: '加载失败', icon: 'none' })
      }
    })
}
const loadMore = () => {
  if (loadStatus.value === 'nomore' || loadStatus.value === 'loading') return
  loadStatus.value = 'loading'
  page.current++
  getList()
}
watch(activeTab, () => {
  page.current = 1
  loadStatus.value = 'loadmore'
  stockRecordTypeOptions.value = []
  getList()
})
const handleQuery = () => {
  page.current = 1
  loadStatus.value = 'loadmore'
  getList()
}
const goDetail = (item) => {
  if (!item.id) return
  try {
    uni.setStorageSync('receiptDetailItem', JSON.stringify({
      item,
      type: currentType()
    }))
  } catch (e) {}
  uni.navigateTo({
    url: '/pages/inventoryManagement/receiptManagement/view?id=' + item.id
  })
}
// 打开日期选择器(简单可靠)
const openDatePickerHandler = () => {
  // 若已有选中日期,用它初始化;否则用今天
  dateValue.value = searchForm.value.timeStr
    ? dayjs(searchForm.value.timeStr, 'YYYY-MM-DD').valueOf()
    : Date.now()
  showDatePicker.value = true
}
// 日期选择确认(与其他页一致:拿时间戳 -> YYYY-MM-DD)
const onDateConfirm = (e) => {
  searchForm.value.timeStr = formatDateToYMD(e.value)
  showDatePicker.value = false
  handleQuery()
}
// 新增入库
const handleAdd = () => {
  formDiaManual.value?.openDialog('add')
}
// 编辑
const handleEdit = (item) => {
  formDiaManual.value?.openDialog('edit', item)
}
// 删除单条
const handleDeleteSingle = (item) => {
  // 检查是否是本人创建
  if (item.createBy !== userStore.nickName) {
    uni.showToast({
      title: '不可删除他人维护的数据',
      icon: 'none'
    })
    return
  }
  uni.showModal({
    title: '删除',
    content: '确认删除该入库记录吗?',
    success: (res) => {
    content: '确认删除该条入库记录?',
    success: res => {
      if (res.confirm) {
        delStockInCustom({ ids: [item.id] }).then(() => {
          uni.showToast({
            title: '删除成功',
            icon: 'success'
        batchDeleteStockInRecords([item.id])
          .then(() => {
            uni.showToast({ title: '删除成功', icon: 'success' })
            getList()
          })
          getList()
        }).catch(() => {
          uni.showToast({
            title: '删除失败',
            icon: 'none'
          .catch(() => {
            uni.showToast({ title: '删除失败', icon: 'none' })
          })
        })
      }
    }
  })
}
const goBack = () => uni.navigateBack()
onShow(() => {
  getList()
  if (activeTab.value !== 'custom') getList()
})
onReachBottom(() => {
  loadMore()
})
</script>
<style scoped lang="scss">
.stock-in-page {
<style lang="scss" scoped>
.receipt-page {
  min-height: 100vh;
  background: #f5f5f5;
  padding-bottom: 80px;
  padding-bottom: 40rpx;
}
.tabs-wrap {
  display: flex;
  background: #fff;
  padding: 24rpx;
  gap: 24rpx;
}
.tab-item {
  flex: 1;
  text-align: center;
  padding: 20rpx;
  border-radius: 12rpx;
  background: #f0f0f0;
  font-size: 28rpx;
  color: #666;
}
.tab-item.active {
  background: #2979ff;
  color: #fff;
}
.search-section {
  background: #fff;
  padding: 16px;
  margin-bottom: 12px;
  margin: 24rpx;
  padding: 24rpx;
  border-radius: 16rpx;
}
.search-bar {
.search-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
  margin-bottom: 20rpx;
}
.search-input {
  flex: 1;
}
.search-button {
  width: 44px;
  height: 44px;
.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
.btn-search {
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f5f5f5;
  border-radius: 8px;
}
.date-filter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  background: #f5f5f5;
  border-radius: 8px;
}
.date-text {
  font-size: 14px;
  color: #666;
}
.stock-list {
  padding: 0 16px;
}
.stock-item {
  background: #fff;
  border-radius: 12px;
  padding: 16px;
  margin-bottom: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.item-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}
.item-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
.batch-icon {
  width: 32px;
  height: 32px;
  width: 180rpx;
  min-height: 72rpx;
  flex-shrink: 0;
  padding: 20rpx 24rpx;
  background: #2979ff;
  border-radius: 8px;
  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;
}
.btn-search text {
  line-height: 1;
  vertical-align: middle;
}
/* 按钮内图标与文字垂直水平都居中 */
:deep(.btn-search-inner > *),
:deep(.btn-search > *) {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.batch-text {
  font-size: 14px;
.list-section {
  padding: 0 24rpx;
}
.card-item {
  background: #fff;
  border-radius: 16rpx;
  padding: 24rpx;
  margin-bottom: 24rpx;
  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.card-header {
  padding: 8rpx 0;
}
.header-main {
  display: flex;
  flex-direction: column;
  gap: 8rpx;
}
.product-name {
  font-size: 30rpx;
  font-weight: 500;
  color: #333;
}
.time-text {
  font-size: 12px;
.inbound-date {
  font-size: 24rpx;
  color: #999;
}
.item-details {
  margin: 12px 0;
}
.detail-row {
.card-body .row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 0;
  padding: 12rpx 0;
  font-size: 26rpx;
}
.detail-label {
  font-size: 14px;
  color: #666;
}
.detail-value {
  font-size: 14px;
  color: #333;
  text-align: right;
  flex: 1;
  margin-left: 12px;
}
.detail-value.highlight {
  color: #2979ff;
  font-weight: 500;
}
.detail-value.price {
  color: #ff6b00;
  font-weight: 500;
}
.item-actions {
.card-body .l { color: #666; }
.card-body .r { color: #333; }
.card-body .r.highlight { color: #2979ff; font-weight: 500; }
.card-actions {
  margin-top: 16rpx;
  padding-top: 16rpx;
  border-top: 1rpx solid #eee;
  display: flex;
  gap: 12px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid #f5f5f5;
  justify-content: center;
  align-items: center;
}
.btn-delete {
  font-size: 28rpx;
  color: #f56c6c;
  padding: 12rpx 32rpx;
}
.no-data {
  text-align: center;
  padding: 60px 0;
  padding: 80rpx 0;
  color: #999;
  font-size: 14px;
  font-size: 28rpx;
}
.fab-button {
  position: fixed;
  right: 20px;
  bottom: 80px;
  width: 56px;
  height: 56px;
  background: #2979ff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 12px rgba(41, 121, 255, 0.4);
  z-index: 999;
.load-more-wrap {
  padding: 24rpx 0 40rpx;
}
</style>