| | |
| | | <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> |