gaoluyang
2025-11-21 bd712637441c43d6e23a8577b748a5d2b3365745
添加自定义出库页面
已添加2个文件
已修改2个文件
658 ■■■■■ 文件已修改
src/api/inventoryManagement/stockManage.js 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/issueManagement/index.vue 564 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockManage.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
import request from "@/utils/request";
// æŸ¥è¯¢åº“存信息列表
export const getStockManagePage = (params) => {
    return request({
        url: "/stockin/listPageCopy",
        method: "get",
        params,
    });
};
// æŸ¥è¯¢ç”Ÿäº§å…¥åº“库存信息列表
export const getStockManagePageByProduction = (params) => {
    return request({
        url: "/stockin/listPageCopyByProduction",
        method: "get",
        params,
    });
};
// æŸ¥è¯¢è‡ªå®šä¹‰å…¥åº“库存信息列表
export const getStockManagePageByCustom = (params) => {
    return request({
        url: "/stockin/listPageCopyByCustom",
        method: "get",
        params,
    });
};
// ä¿®æ”¹åº“存信息
export const updateStockManage = (data) => {
    return request({
        url: "/stockmanagement/update",
        method: "put",
        data,
    });
};
// åˆ é™¤åº“存信息
export function delStockManage(ids) {
    return request({
        url: '/stockin/del',
        method: 'post',
        data: ids
    })
}
// å¯¼å‡ºåº“存信息
export function exportStockManage(query) {
    return request({
        url: '/stockmanagement/export',
        method: 'get',
        params: query,
        responseType: 'blob'
    })
}
// å‡ºåº“管理-领用接口
export const stockOut = (data) => {
    return request({
        url: '/stockmanagement/stockout',
        method: 'post',
        data: data
    })
}
//根据id获取库存信息
export function getStockManageById(id) {
    return request({
        url: '/stockmanagement/' + id,
        method: 'get'
    })
}
//
src/pages.json
@@ -469,6 +469,13 @@
        "navigationBarTitleText": "新增入库",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/inventoryManagement/issueManagement/index",
      "style": {
        "navigationBarTitleText": "自定义出库",
        "navigationStyle": "custom"
      }
    }
  ],
  "subPackages": [
src/pages/index.vue
@@ -125,7 +125,7 @@
        <view class="common-module collaboration-module">
            <view class="module-header">
                <view class="module-title-container">
                    <text class="module-title">入库管理</text>
                    <text class="module-title">仓储物流</text>
                </view>
            </view>
            <view class="module-content">
@@ -328,6 +328,10 @@
        icon: '/static/images/icon/rukuguanli@2x.png',
        label: '自定义入库',
    },
    {
        icon: '/static/images/icon/rukuguanli@2x.png',
        label: '自定义出库',
    },
]);
// ç”Ÿäº§ç®¡æŽ§åŠŸèƒ½æ•°æ®
@@ -493,6 +497,11 @@
                url: '/pages/inventoryManagement/receiptManagement/index'
            });
            break;
        case '自定义出库':
            uni.navigateTo({
                url: '/pages/inventoryManagement/issueManagement/index'
            });
            break;
        case '生产订单':
            uni.navigateTo({
                url: '/pages/productionManagement/productionOrder/index'
src/pages/inventoryManagement/issueManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,564 @@
<template>
  <view class="stock-out-page">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <PageHeader title="自定义出库" @back="goBack" />
    <!-- æœç´¢åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <u-input
            v-model="searchForm.supplierName"
            placeholder="请输入供应商名称"
            border="none"
            clearable
          />
        </view>
        <view class="search-button" @click="handleQuery">
          <u-icon name="search" size="24" color="#999"></u-icon>
        </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>
    <!-- æ—¥æœŸé€‰æ‹©å™¨ -->
    <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>
    <!-- æ•°æ®åˆ—表 -->
    <view class="stock-list" v-if="tableData.length > 0">
      <view v-for="(item, index) in tableData" :key="item.id" class="stock-item">
        <view class="item-header">
          <view class="item-left">
            <view class="batch-icon">
              <u-icon name="file-text" size="16" color="#ffffff"></u-icon>
            </view>
            <text class="batch-text">{{ item.inboundBatches || '未知批次' }}</text>
          </view>
          <view class="item-right">
            <text class="time-text">{{ item.inboundDate || item.createTime || '-' }}</text>
          </view>
        </view>
        <u-divider></u-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 highlight">{{ item.inboundNum || 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">含税单价</text>
            <text class="detail-value">Â¥{{ item.taxInclusiveUnitPrice || 0 }}</text>
          </view>
          <view class="detail-row">
            <text class="detail-label">含税总价</text>
            <text class="detail-value price">Â¥{{ item.taxInclusiveTotalPrice || 0 }}</text>
          </view>
        </view>
        <view class="item-actions">
          <u-button type="primary" size="small" @click="openForm(item)">领用</u-button>
          <u-button type="warning" size="small" plain @click="handleOut">导出</u-button>
        </view>
      </view>
    </view>
    <view v-else class="no-data">
      <text>暂无数据</text>
    </view>
    <!-- åŠ è½½æ›´å¤š -->
    <view class="load-more" v-if="tableData.length > 0">
      <u-loadmore :status="loadStatus" @loadmore="loadMore" />
    </view>
    <!-- å‡ºåº“表单弹窗 -->
    <u-popup
      v-model="dialogFormVisible"
      mode="center"
      :closeable="true"
      @close="closeDia"
      round="10"
    >
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">新增出库</text>
        </view>
        <view class="popup-body">
          <u-form :model="form" :rules="rules" ref="formRef" labelWidth="80">
            <u-form-item label="出库数量" prop="inboundQuantity" borderBottom>
              <u-input
                v-model="form.inboundQuantity"
                placeholder="请输入出库数量"
                type="number"
                border="none"
              />
            </u-form-item>
            <u-form-item label="出库日期" prop="inboundTime" borderBottom>
              <u-input
                v-model="form.inboundTime"
                placeholder="请选择出库日期"
                border="none"
                readonly
                @click="showOutDatePicker = true"
              >
                <template #suffix>
                  <u-icon name="calendar" size="18"></u-icon>
                </template>
              </u-input>
            </u-form-item>
            <u-form-item label="出库人" prop="nickName" borderBottom>
              <u-select
                v-model="form.nickName"
                :list="userList"
                labelName="nickName"
                valueName="userId"
                placeholder="请选择出库人"
              ></u-select>
            </u-form-item>
          </u-form>
        </view>
        <view class="popup-footer">
          <u-button type="primary" @click="submitForm" size="normal">确认</u-button>
          <u-button @click="closeDia" size="normal" plain>取消</u-button>
        </view>
      </view>
    </u-popup>
    <!-- å‡ºåº“日期选择器 -->
    <u-datetime-picker
      v-model="form.inboundTime"
      :show="showOutDatePicker"
      mode="date"
      @confirm="showOutDatePicker = false"
      @cancel="showOutDatePicker = false"
    />
  </view>
</template>
<script setup>
import { ref, reactive, toRefs, onMounted, getCurrentInstance, nextTick } from 'vue'
import dayjs from 'dayjs'
import PageHeader from '@/components/PageHeader.vue'
import useUserStore from '@/store/modules/user'
import { formatDateToYMD } from '@/utils/ruoyi'
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { getInPageByCustom } from "@/api/inventoryManagement/stockIn.js";
import { stockOut } from "@/api/inventoryManagement/stockManage.js";
const userStore = useUserStore()
const { proxy } = getCurrentInstance()
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
// æ‰“开日期选择器(简单可靠)
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 tableData = ref([])
const userList = ref([])
const tableLoading = ref(false)
const dialogFormVisible = ref(false)
const showDatePicker = ref(false)
const showOutDatePicker = ref(false)
const loadStatus = ref('loadmore')
const dateValue = ref(new Date().getTime())
const page = reactive({
  current: 1,
  size: 10,
})
const total = ref(0)
const currentRowId = ref(null)
const currentRowNum = ref(0)
const data = reactive({
  searchForm: {
    supplierName: '',
    timeStr: '',
  },
  form: {
    inboundQuantity: '',
    inboundTime: '',
    nickName: '',
  },
  rules: {
    inboundQuantity: [
      { required: true, message: '请输入出库数量', trigger: 'blur' },
      {
        validator: (rule, value, callback) => {
          if (value && (value <= 0 || value > currentRowNum.value)) {
            callback(new Error('请填入有效数字'))
          } else {
            callback()
          }
        },
        trigger: 'blur'
      }
    ],
    inboundTime: [
      { required: true, message: '请选择出库日期', trigger: 'change' }
    ],
    nickName: [
      { required: true, message: '请选择出库人', trigger: 'change' }
    ]
  }
})
const { searchForm, form, rules } = toRefs(data)
// æŸ¥è¯¢åˆ—表
const handleQuery = () => {
  page.current = 1
  getList()
}
const getList = () => {
  tableLoading.value = true
  const params = {
    ...page,
    supplierName: searchForm.value.supplierName,
    timeStr: searchForm.value.timeStr
  }
  getInPageByCustom(params).then(res => {
    tableLoading.value = false
    if (page.current === 1) {
      tableData.value = res.data.records || []
    } else {
      tableData.value = [...tableData.value, ...(res.data.records || [])]
    }
    total.value = res.data.total || 0
    // æ›´æ–°åŠ è½½çŠ¶æ€
    if (tableData.value.length >= total.value) {
      loadStatus.value = 'nomore'
    } else {
      loadStatus.value = 'loadmore'
    }
  }).catch(() => {
    tableLoading.value = false
    loadStatus.value = 'error'
  })
}
// åŠ è½½æ›´å¤š
const loadMore = () => {
  if (loadStatus.value === 'nomore') return
  loadStatus.value = 'loading'
  page.current++
  getList()
}
// æ‰“开出库表单
const openForm = async (row) => {
  dialogFormVisible.value = true
  currentRowId.value = row.id
  currentRowNum.value = row.inboundNum || 0
  // åˆå§‹åŒ–表单数据
  form.value = {
    inboundQuantity: '',
    inboundTime: getCurrentDate(),
    nickName: '',
  }
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  try {
    const userLists = await userListNoPageByTenantId()
    userList.value = userLists.data.map(item => ({
      ...item,
      label: item.nickName,
      value: item.userId
    }))
  } catch (error) {
    console.error('加载用户列表失败:', error)
    userList.value = []
  }
}
// æäº¤è¡¨å•
const submitForm = () => {
  proxy.$refs.formRef.validate().then(valid => {
    if (valid && currentRowId.value) {
      const outData = {
        id: currentRowId.value,
        salesLedgerProductId: 0,
        quantity: form.value.inboundQuantity,
        time: form.value.inboundTime,
        userId: form.value.nickName,
        type: 3 // è‡ªå®šä¹‰å‡ºåº“类型
      }
      stockOut(outData).then(res => {
        uni.$u.toast('提交成功')
        closeDia()
        getList()
      }).catch(err => {
        uni.$u.toast('出库失败')
      })
    }
  }).catch(err => {
    console.log('表单验证失败:', err)
  })
}
// å…³é—­å¼¹çª—
const closeDia = () => {
  dialogFormVisible.value = false
  proxy.$refs.formRef.resetFields()
}
// å¯¼å‡º
const handleOut = () => {
  uni.showModal({
    title: '导出',
    content: '是否确认导出?',
    success: (res) => {
      if (res.confirm) {
        proxy.download("/stockin/exportTwo", {}, '自定义出库台账.xlsx')
      }
    }
  })
}
// èŽ·å–å½“å‰æ—¥æœŸ
function getCurrentDate() {
  const today = new Date()
  const year = today.getFullYear()
  const month = String(today.getMonth() + 1).padStart(2, '0')
  const day = String(today.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}
onMounted(() => {
  getList()
})
</script>
<style scoped lang="scss">
.stock-out-page {
  min-height: 100vh;
  background: #f5f5f5;
  padding-bottom: 80px;
}
.search-section {
  background: #fff;
  padding: 16px;
  margin-bottom: 12px;
}
.search-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}
.search-input {
  flex: 1;
}
.search-button {
  width: 44px;
  height: 44px;
  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;
  background: #2979ff;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.batch-text {
  font-size: 14px;
  font-weight: 500;
  color: #333;
}
.time-text {
  font-size: 12px;
  color: #999;
}
.item-details {
  margin: 12px 0;
}
.detail-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 0;
}
.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 {
  display: flex;
  gap: 12px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid #f5f5f5;
}
.no-data {
  text-align: center;
  padding: 60px 0;
  color: #999;
  font-size: 14px;
}
.load-more {
  padding: 20px 16px;
}
.popup-content {
  width: 600rpx;
  background: #ffffff;
  border-radius: 20rpx;
  .popup-header {
    padding: 40rpx 30rpx 20rpx;
    text-align: center;
    border-bottom: 1rpx solid #f0f0f0;
    .popup-title {
      font-size: 32rpx;
      font-weight: bold;
      color: #333;
    }
  }
  .popup-body {
    padding: 30rpx;
  }
  .popup-footer {
    padding: 30rpx;
    display: flex;
    gap: 20rpx;
    border-top: 1rpx solid #f0f0f0;
    .u-button {
      flex: 1;
    }
  }
}
</style>